Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8555138366 | ||
|
|
b14006a826 | ||
|
|
48a751b508 | ||
|
|
8dcfbcfaf7 | ||
|
|
55f6dfaee7 | ||
|
|
79b3290002 | ||
|
|
1cbe8af998 | ||
|
|
852703c4ef | ||
|
|
71f01b1c5c | ||
|
|
4b3b7d86fd | ||
|
|
1c27693dbe | ||
|
|
ff7be43861 | ||
|
|
9927c40963 | ||
|
|
236a0fd300 | ||
|
|
36a66d3eac | ||
|
|
317411ce60 | ||
|
|
3c6529bc99 | ||
|
|
061390df80 | ||
|
|
b4fd1c738f | ||
|
|
c7d6c6c5e5 | ||
|
|
31f094818e | ||
|
|
40afa53337 | ||
|
|
cb98d721c5 | ||
|
|
6ba5137ea1 | ||
|
|
5377674a30 | ||
|
|
f6ed462bce | ||
|
|
890223021a | ||
|
|
b1f874b3c8 | ||
|
|
2dd9ee19e9 | ||
|
|
aa203c546c | ||
|
|
8f31031244 | ||
|
|
84e367b79f | ||
|
|
0854e25a66 | ||
|
|
0c299db26f | ||
|
|
f267d06536 | ||
|
|
494b027513 | ||
|
|
35c58ff631 | ||
|
|
e87cd6d73e | ||
|
|
05b7a1181c | ||
|
|
2e91fe7ae4 | ||
|
|
9c3de358b3 | ||
|
|
b1e73f0761 | ||
|
|
222aa75a1d | ||
|
|
caa78d7c45 | ||
|
|
5edcaa373c | ||
|
|
08e412b32f | ||
|
|
4269946c30 | ||
|
|
607817302b | ||
|
|
dd39fa6113 |
29
CHANGELOG.md
29
CHANGELOG.md
@@ -6,6 +6,31 @@ Date format : day/month/year
|
||||
> - `foundry-version`: Stick to the major version of FoundryVTT.
|
||||
> - `system-version`: System functionalities and Fixes.
|
||||
|
||||
## 1.14.0 - ??/04/2026 - Foundry v14 Compatibility
|
||||
__! Be certain to carefully back up any critical user data before installing this update !__
|
||||
- Updated the System to FoundryVTT v14.
|
||||
- Fix List without bullet on item sheets.
|
||||
- Fix black embedded links displays on Tooltips.
|
||||
|
||||
## 1.13.4 - 01/03/2026 - UI Polish, Compendium Upgrades
|
||||
Welcoming Litasa as a maintainer for the system!
|
||||
|
||||
- Fixing type in Courts of Stone title, thanks to SagaTympana (!51).
|
||||
- Update to the [development wiki](https://gitlab.com/teaml5r/l5r5e/-/wikis/home), thanks to Norman Briggs (!52)
|
||||
- Updating the sidebar icons to show L5r5e specific ones (#72)(!53) (Litasa)
|
||||
- Adding incremental buttons for honor, glory, and status (#69)(!54) (Litasa)
|
||||
- New rank is now shown directly when completing a school rank (#64)(!57) (Litasa)
|
||||
- Adding Starting items to Compendiums that was missing. Split Poison and Omamori into individual items(!58) (Litasa)
|
||||
- Some combinations of light and dark theme made the GM Toolbox and GM Monitor hard/impossible to use. Fixed now (#75)(!59) (Litasa)
|
||||
- Conditions now show in the top of dice-picker and roll-n-keep (related to #74)(!60) (Litasa)
|
||||
- Compendium filter is now a lot "snappier". New search box and now able to multi select elements/ranks/rarity. (!61) (Litasa)
|
||||
- Adding selection circle to Combat encounter (!62) (Litasa)
|
||||
|
||||
## 1.13.3 - 01/02/2026 - Tactical Grid & Fixes
|
||||
- Updated demeanors from books up to Imperfect Land (included), thanks to Olivier Brencklé (!48).
|
||||
- Added Tactical Grid Range Band, thanks to Litasa (!49).
|
||||
- Fix Title advancement auto-name/icon, thanks to SagaTympana (!50).
|
||||
|
||||
## 1.13.2 - 18/10/2025 - Conditions Icons & Fixes
|
||||
- Fix Actor Sheet for pressing key `Enter` in input trigger `no active Encounter...` message.
|
||||
- Fix Compendium `Astrolab` is duplicate with `Mantis Clan` and `Children of the Five Winds`. Renamed the `cotfw` version to `Astrolabe (Unicorn)`.
|
||||
@@ -14,8 +39,8 @@ Date format : day/month/year
|
||||
- Spanish language updated thanks to Alejabarr.
|
||||
|
||||
## 1.13.1 - 21/09/2025 - Conditions & Fixes
|
||||
- Fix for Clicking on items doesn't show item window (#65 Thx to Litasa)
|
||||
- Fix for fade configuration (#66)
|
||||
- Fix for Clicking on items doesn't show item window (#65 Thx to Litasa).
|
||||
- Fix for fade configuration (#66).
|
||||
- Added some Tooltips loading optimizations (#62 Thanks to KitCat).
|
||||
- Added some Properties loading optimizations (#63 Thanks to KitCat).
|
||||
- Conditions changes :
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||

|
||||
|
||||
[](https://ko-fi.com/vlyan)
|
||||
[](https://foundryvtt.com/)
|
||||
[](https://foundryvtt.com/)
|
||||
[](https://forge-vtt.com/bazaar#package=l5r5e)
|
||||
[](https://www.foundryvtt-hub.com/package/l5r5e/)
|
||||
[](https://www.foundryvtt-hub.com/package/l5r5e/)
|
||||
@@ -18,13 +18,14 @@ See the [Wiki page - Installation](https://gitlab.com/teaml5r/l5r5e/-/wikis/user
|
||||
|
||||
|
||||
## Current L5R team (alphabetical order)
|
||||
- Carter (compendiums, adventure adaptation)
|
||||
- Litasa (development)
|
||||
- Vlyan (development)
|
||||
|
||||
|
||||
## Historical L5R team (alphabetical order)
|
||||
- Carter (compendiums, adventure adaptation)
|
||||
- Hrunh (compendiums, pre-gen characters adaptation)
|
||||
- Litasa (development)
|
||||
- Mandar (development)
|
||||
- Sasmira (contributor)
|
||||
- Vlyan (development)
|
||||
|
||||
11226
package-lock.json
generated
11226
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -19,17 +19,18 @@
|
||||
},
|
||||
"homepage": "https://gitlab.com/teaml5r/l5r5e#readme",
|
||||
"devDependencies": {
|
||||
"@foundryvtt/foundryvtt-cli": "^3.0.3",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"browser-sync": "^2.27.10",
|
||||
"browser-sync": "^3.0.4",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp": "^5.0.1",
|
||||
"gulp-autoprefixer": "^8.0.0",
|
||||
"gulp-sass": "^5.1.0",
|
||||
"lint-staged": "^10.5.1",
|
||||
"sass": "^1.56.1",
|
||||
"prettier": "^2.7.1"
|
||||
"prettier": "^2.7.1",
|
||||
"sass": "^1.56.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": "eslint --cache --fix",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
Created by potrace 1.15, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,1264.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
fill="currentColor" stroke="none">
|
||||
<path d="M5925 12625 c-248 -21 -380 -39 -378 -51 1 -7 -56 -18 -148 -29 -83
|
||||
-9 -190 -25 -237 -35 -244 -53 -454 -105 -602 -151 -91 -28 -210 -64 -264 -81
|
||||
-465 -138 -1038 -415 -1493 -719 -234 -157 -585 -429 -773 -600 -215 -196
|
||||
|
||||
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
@@ -1,13 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.1.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="48.2px" height="48.2px" viewBox="0 0 48.2 48.2" style="enable-background:new 0 0 48.2 48.2;" xml:space="preserve">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="48.2px" height="48.2px" viewBox="0 0 48.2 48.2" xml:space="preserve">
|
||||
<defs>
|
||||
<linearGradient id="iconGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#E0E0E0;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#FFFFFF;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<style type="text/css">
|
||||
.st0{fill:#030104;}
|
||||
/* Gradient fill with contrasting stroke */
|
||||
.fill-gradient{fill:url(#iconGradient);}
|
||||
.stroke-contrast{stroke:#2a2a2a;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M43,40c-0.9-7.2-2.3-13.9-5.7-20.2c-1-1.9-2.6-3.5-3.8-5.3c-0.2-0.3-0.5-0.7-0.7-1.1c0.7-0.6,1.4-1.3,2.1-2
|
||||
<path class="fill-gradient stroke-contrast" d="M43,40c-0.9-7.2-2.3-13.9-5.7-20.2c-1-1.9-2.6-3.5-3.8-5.3c-0.2-0.3-0.5-0.7-0.7-1.1c0.7-0.6,1.4-1.3,2.1-2
|
||||
c0.8-0.8,1.5-1.5,2.1-2.3c0.7-0.8,1.2-1.5,1.7-2.1c1-1.3,1.5-2.3,1.5-2.3s-1,0.5-2.3,1.5c-0.7,0.5-1.4,1.1-2.1,1.7
|
||||
c-0.8,0.7-1.5,1.4-2.3,2.1c-0.4,0.4-0.9,0.9-1.3,1.4c-0.7-3.4-3.5-6.2-7-6.6c-3.7-0.5-7.6,1.7-8.5,5.4c-0.1,0.5-0.3,1-0.4,1.4
|
||||
c-0.5-0.5-1-1-1.5-1.5C14,9.3,13.2,8.6,12.5,8c-0.8-0.7-1.5-1.2-2.1-1.7C9,5.3,8,4.8,8,4.8s0.5,1,1.5,2.3c0.5,0.7,1.1,1.4,1.7,2.1
|
||||
@@ -17,10 +24,10 @@
|
||||
c0.5,0.1,1,0,1.4-0.2c0.4-0.2,0.8-0.6,1.1-1.1c0.5-0.7,0.7-0.3,1.3,0.6c0.5,0.9,1.6,1.2,3,1.4c2.9,0.4,2.8,0.6,2.5,3.5
|
||||
c0,0.5,0,1-0.1,1.4c-0.5,2.3,0.1,5.1-1.8,6.9c-0.8,0.7-1.6,1.1-1.5,1.2c0.1,0.1,1.1,0,2.1-0.1c2.4-0.3,4.9-0.7,7.4-1
|
||||
C42.3,42.1,43.1,41.1,43,40z M21.3,8.2C20.2,9,19.2,9.3,19,9s0.4-1.2,1.5-1.9c1-0.7,2.1-1.1,2.3-0.7C23,6.6,22.3,7.5,21.3,8.2z"/>
|
||||
<path class="st0" d="M32,35.9c0-0.2-0.9-0.2-1.9-0.1c-1,0.1-1.8,0.4-1.8,0.7c0,0.2,0.9,0.3,1.9,0.1C31.3,36.4,32,36.1,32,35.9z"/>
|
||||
<path class="st0" d="M18.2,35.8c-0.9-0.2-1.7-0.1-1.8,0.1c0,0.2,0.7,0.5,1.6,0.7c0.9,0.2,1.7,0.1,1.8-0.1
|
||||
<path class="fill-gradient stroke-contrast" d="M32,35.9c0-0.2-0.9-0.2-1.9-0.1c-1,0.1-1.8,0.4-1.8,0.7c0,0.2,0.9,0.3,1.9,0.1C31.3,36.4,32,36.1,32,35.9z"/>
|
||||
<path class="fill-gradient stroke-contrast" d="M18.2,35.8c-0.9-0.2-1.7-0.1-1.8,0.1c0,0.2,0.7,0.5,1.6,0.7c0.9,0.2,1.7,0.1,1.8-0.1
|
||||
C19.9,36.3,19.2,36,18.2,35.8z"/>
|
||||
<path class="st0" d="M25.3,40.3c-0.1,0-0.3,0-0.5-0.1c-0.1,0-0.2-0.1-0.3-0.1c-0.1,0-0.2,0-0.2-0.1c-0.1,0-0.3-0.1-0.4,0
|
||||
<path class="fill-gradient stroke-contrast" d="M25.3,40.3c-0.1,0-0.3,0-0.5-0.1c-0.1,0-0.2-0.1-0.3-0.1c-0.1,0-0.2,0-0.2-0.1c-0.1,0-0.3-0.1-0.4,0
|
||||
c-0.1,0-0.3,0-0.4,0c-0.1,0-0.2,0.1-0.2,0.1c-0.1,0-0.2,0.1-0.2,0.1c-0.2,0.1-0.4,0-0.5,0.1c-0.1,0-0.2,0.1-0.2,0.1s0,0.1,0,0.3
|
||||
c0,0.2,0,0.3,0.1,0.6c0.1,0.1,0.1,0.2,0.3,0.3c0.1,0.1,0.2,0.2,0.4,0.3c0.1,0.1,0.3,0.2,0.4,0.2c0.2,0,0.3,0.1,0.5,0.1
|
||||
c0.2,0,0.3,0,0.5-0.1c0.2,0,0.3-0.1,0.4-0.2c0.3-0.1,0.5-0.4,0.6-0.6c0.1-0.2,0.2-0.4,0.2-0.6c0-0.2,0-0.2,0-0.2
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.1.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="48.2px" height="48.2px" viewBox="0 0 48.2 48.2" style="enable-background:new 0 0 48.2 48.2;" xml:space="preserve">
|
||||
width="48.2px" height="48.2px" viewBox="-1 -1 50.2 50.2" style="enable-background:new -1 -1 50.2 50.2;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#1D1C1A;}
|
||||
.st0{fill:#030104;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M48.7,4.8c0,0-0.1,0-0.2,0.1c-0.9,0.6-4.4,2-9.3,3.1c-4.6,1-10.8,1.8-16.7,2.3c-6.2,0.6-14.9,0.8-21.3-0.5
|
||||
|
||||
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
@@ -262,7 +262,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Journal of observations",
|
||||
"id": "Journal of Observations",
|
||||
"name": "Journal d'observations",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
@@ -972,6 +972,198 @@
|
||||
"source_reference": {
|
||||
"page": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Glass Ornament (Dragonfly)",
|
||||
"name": "Le Verre de Libellule",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Arrows: Armor-Piercing",
|
||||
"name": "Flèches : Perce-armure",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": "236"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Arrows: Flesh-Cutter",
|
||||
"name": "Flèches : Fouilleuse d’entrailles",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": "236"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Arrows: Humming-Bulb",
|
||||
"name": "Flèches : Bulbe bourdonnant",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": "236"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Journal",
|
||||
"name": "Journal",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": "66"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Smithing hammer",
|
||||
"name": "Marteau de forgeron",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Sumai Garb",
|
||||
"name": "Tenue de Sumai",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": "76"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Drafting Paper",
|
||||
"name": "Papier à dessin",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Fine set of Chisels",
|
||||
"name": "Ensemble de burins de grande qualité",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Omamori (Boon of Fukurokujin)",
|
||||
"name": "Omamori (Bienfait de Fukurokujin)",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": "243"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Omamori (Boon of Bishamon)",
|
||||
"name": "Omamori (Bienfait de Bishamon)",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": "243"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Omamori (Boon of Benten)",
|
||||
"name": "Omamori (Bienfait de Benten)",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": "243"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Poison - Noxious Poison (One Vial)",
|
||||
"name": "Poison - Toxines (un flacon)",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": "244"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Poison - Fire Biter (One Vial)",
|
||||
"name": "Poison - Morsure Brûlante (un flacon)",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": "244"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Poison - Night Milk (One Vial)",
|
||||
"name": "Poison - Lait de la Nuit (un flacon)",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": "244"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Blanket",
|
||||
"name": "Couverture",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": "245"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Bowl",
|
||||
"name": "Bol",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": "245"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Flint and Tinder",
|
||||
"name": "Silex et amadou",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": "245"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Furoshiki",
|
||||
"name": "Furoshiki",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": "245"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "The Obsidian Journal",
|
||||
"name": "Le Journal d’obsidienne",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": "128"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Pouch of Insence",
|
||||
"name": "Bourse d’Encens",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": "85"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Religious texts",
|
||||
"name": "Textes religieux",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": "86"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Small Sachel of Ingredients",
|
||||
"name": "Petit sachet d'ingrédients",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": "80"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Blessed Glass vial",
|
||||
"name": "Fiole en verre bénie",
|
||||
"description": "",
|
||||
"source_reference": {
|
||||
"page": "80"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
},
|
||||
"Compendium": {
|
||||
"HideDisabledSources": {
|
||||
"Title": "[Compendium] Hide sources filter without reference",
|
||||
"Hint": "Hide empty source with no elements in source filter."
|
||||
"Title": "[Compendium] Hide unavailable sources",
|
||||
"Hint": "Hide sources that have no available content from the source filter dropdown."
|
||||
},
|
||||
"HideEmptySourcesFromPlayers": {
|
||||
"Title": "[Compendium] Hide elements with empty reference",
|
||||
@@ -136,6 +136,7 @@
|
||||
"player_filter_label": "Player filter",
|
||||
"player_filter_tooltip": "Apply player filter",
|
||||
"already_in_filter": "Already in filter",
|
||||
"no_results": "Not Found",
|
||||
"sources_categories": {
|
||||
"rules": "Rules",
|
||||
"adventures": "Adventures",
|
||||
@@ -492,7 +493,10 @@
|
||||
"rarity_modifier": "Rarity modifier",
|
||||
"item_pattern": "Item Patterns",
|
||||
"signature_scroll": "Signature Scrolls",
|
||||
"school_curriculum_journal": "Drop curriculum's journal in sheet to link it"
|
||||
"school_curriculum_journal": "Drop curriculum's journal in sheet to link it",
|
||||
"warning": {
|
||||
"total_less_then_spent": "Total Experience is less then Used Experience."
|
||||
}
|
||||
},
|
||||
"character_types": {
|
||||
"character": "Player Character",
|
||||
@@ -705,6 +709,7 @@
|
||||
"demeanor": {
|
||||
"adaptable": "Adaptable",
|
||||
"aggressive": "Aggressive",
|
||||
"alluring": "Alluring",
|
||||
"ambitious": "Ambitious",
|
||||
"amiable": "Amiable",
|
||||
"analytical": "Analytical",
|
||||
@@ -713,6 +718,7 @@
|
||||
"assertive": "Assertive",
|
||||
"beguiling": "Beguiling",
|
||||
"bitter": "Bitter",
|
||||
"bloodthirsty": "Bloodthirsty",
|
||||
"bold": "Bold",
|
||||
"calculating": "Calculating",
|
||||
"calm": "Calm",
|
||||
@@ -723,37 +729,68 @@
|
||||
"confused": "Confused",
|
||||
"courageous": "Courageous",
|
||||
"cowardly": "Cowardly",
|
||||
"crestfallen": "Crestfallen",
|
||||
"curious": "Curious",
|
||||
"defensive": "Defensive",
|
||||
"dependable": "Dependable",
|
||||
"detached": "Detached",
|
||||
"determined": "Determined",
|
||||
"devoted": "Devoted",
|
||||
"direct": "Direct",
|
||||
"disheartened": "Disheartened",
|
||||
"dour": "Dour",
|
||||
"duplicitous": "Duplicitous",
|
||||
"effusive": "Effusive",
|
||||
"enraged": "Enraged",
|
||||
"fanatical": "Fanatical",
|
||||
"feral": "Feral",
|
||||
"fervent": "Fervent",
|
||||
"fickle": "Fickle",
|
||||
"fierce": "Fierce",
|
||||
"flighty": "Flighty",
|
||||
"flippant": "Flippant",
|
||||
"friendly": "Friendly",
|
||||
"gruff": "Gruff",
|
||||
"honorable": "Honorable",
|
||||
"hubristic": "Prétentieuse",
|
||||
"hungry": "Hungry",
|
||||
"idealistic": "Idealistic",
|
||||
"imposing": "Imposing",
|
||||
"inquisitive": "Inquisitive",
|
||||
"intense": "Intense",
|
||||
"intimidating": "Intimidating",
|
||||
"irritable": "Irritable",
|
||||
"loyal": "Loyal",
|
||||
"methodical": "Methodical",
|
||||
"meticulous": "Meticulous",
|
||||
"mischievous": "Mischievous",
|
||||
"moon_blessed": "Moon-blessed",
|
||||
"morose": "Morose",
|
||||
"near_feral": "Near feral",
|
||||
"nurturing": "Nurturing",
|
||||
"obsessed": "Obsessed",
|
||||
"obstinate": "Obstinate",
|
||||
"opportunistic": "Opportunistic",
|
||||
"otherworldly": "Otherworldly",
|
||||
"outgoing": "Outgoing",
|
||||
"passionate": "Passionate",
|
||||
"patient": "Patient",
|
||||
"personable": "Personable",
|
||||
"playful": "Playful",
|
||||
"power_hungry": "Power hungry",
|
||||
"proud": "Proud",
|
||||
"refined": "Refined",
|
||||
"reserved": "Reserved",
|
||||
"restrained": "Restrained",
|
||||
"righteous": "Righteous",
|
||||
"scheming": "Scheming",
|
||||
"serene": "Serene",
|
||||
"serious": "Serious",
|
||||
"shrewd": "Shrewd",
|
||||
"sinister": "Sinister",
|
||||
"sociable": "Sociable",
|
||||
"stoic": "Stoic",
|
||||
"starved": "Starved",
|
||||
"stubborn": "Stubborn",
|
||||
"suspicious": "Suspicious",
|
||||
"teasing": "Teasing",
|
||||
@@ -761,7 +798,12 @@
|
||||
"uncertain": "Uncertain",
|
||||
"unenthused": "Unenthused",
|
||||
"vain": "Vain",
|
||||
"wary": "Wary"
|
||||
"vengeful": "Vengeful",
|
||||
"vindictive": "Vindictive",
|
||||
"wary": "Wary",
|
||||
"watchful": "Watchful",
|
||||
"wrathful": "Wrathful",
|
||||
"zealous": "Zealous"
|
||||
},
|
||||
"compendium": {
|
||||
"filter_rank": "Show Rank",
|
||||
@@ -769,14 +811,15 @@
|
||||
"filter": {
|
||||
"rank": "Rank",
|
||||
"rarity": "Rarity",
|
||||
"ring": "Ring"
|
||||
"ring": "Ring",
|
||||
"clear": "Clear Filter"
|
||||
}
|
||||
},
|
||||
"source_reference": {
|
||||
"core_rulebook": "Core Rulebook",
|
||||
"emerald_empire": "Emerald Empire",
|
||||
"shadowlands": "Shadowlands",
|
||||
"court_of_stones": "Court of Stones",
|
||||
"court_of_stones": "Courts of Stone",
|
||||
"path_of_waves": "Path of Waves",
|
||||
"celestial_realms": "Celestial Realms",
|
||||
"fields_of_victory": "Fields of Victory",
|
||||
@@ -799,6 +842,32 @@
|
||||
"the_scroll_or_the_blade": "The Scroll or the Blade",
|
||||
"legacies_of_war": "Legacies of War",
|
||||
"children_of_the_five_winds": "Children of the Five Winds"
|
||||
},
|
||||
"tactical_grid": {
|
||||
"settings": {
|
||||
"title": "Tactical Grid Settings",
|
||||
"label": "Tactical Grid Settings",
|
||||
"hint": "Configures tactical grid range band distances (GM only) and their visual appearance colors and transparency (all users).",
|
||||
"cells": "spaces",
|
||||
"world": {
|
||||
"enabled": "Enable Tactical Grid",
|
||||
"enabled_hint": "Enables or Disable tactical grid for everyone",
|
||||
"start": "Start"
|
||||
},
|
||||
"client": {
|
||||
"color": "Color",
|
||||
"alpha": "Alpha"
|
||||
},
|
||||
"range": "Range {index}",
|
||||
"validate": {
|
||||
"start-too-small": "Must be greater than Range Band {previousRangeIndex} ({previousStart})",
|
||||
"start-too-large": "Must be lower then Range Band {nextRangeIndex} ({nextStart})"
|
||||
},
|
||||
"reset": "Reset to Default",
|
||||
"submit": "Save"
|
||||
},
|
||||
"range_band": "Range Band {band}",
|
||||
"range_abbreviation": "RB {range}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,7 @@
|
||||
"player_filter_label": "Filtro de jugador",
|
||||
"player_filter_tooltip": "Aplicar filtro de jugador",
|
||||
"already_in_filter": "Ya en el filtro",
|
||||
"no_results": "Not Found",
|
||||
"sources_categories": {
|
||||
"rules": "Reglas",
|
||||
"adventures": "Aventuras",
|
||||
@@ -492,7 +493,10 @@
|
||||
"rarity_modifier": "Modificador de rareza",
|
||||
"item_pattern": "Patrones de objetos",
|
||||
"signature_scroll": "Pergaminos espaciales",
|
||||
"school_curriculum_journal": "Arrastra el diario del programa en la hoja para vincularlo"
|
||||
"school_curriculum_journal": "Arrastra el diario del programa en la hoja para vincularlo",
|
||||
"warning": {
|
||||
"total_less_then_spent": "La experiencia total es menor que la experiencia utilizada."
|
||||
}
|
||||
},
|
||||
"character_types": {
|
||||
"character": "Personaje jugador",
|
||||
@@ -705,6 +709,7 @@
|
||||
"demeanor": {
|
||||
"adaptable": "Adaptable",
|
||||
"aggressive": "Agresivo",
|
||||
"alluring": "Alluring",
|
||||
"ambitious": "Ambicioso",
|
||||
"amiable": "Amigable",
|
||||
"analytical": "Analítico",
|
||||
@@ -713,6 +718,7 @@
|
||||
"assertive": "Firme",
|
||||
"beguiling": "Seductor",
|
||||
"bitter": "Amargado",
|
||||
"bloodthirsty": "Bloodthirsty",
|
||||
"bold": "Atrevido",
|
||||
"calculating": "Calculador",
|
||||
"calm": "Calmado",
|
||||
@@ -723,37 +729,68 @@
|
||||
"confused": "Confuso",
|
||||
"courageous": "Valiente",
|
||||
"cowardly": "Cobarde",
|
||||
"crestfallen": "Crestfallen",
|
||||
"curious": "Curioso",
|
||||
"defensive": "Defensive",
|
||||
"dependable": "Fiable",
|
||||
"detached": "Desapegado",
|
||||
"determined": "Determined",
|
||||
"devoted": "Devoted",
|
||||
"direct": "Direct",
|
||||
"disheartened": "Desanimado",
|
||||
"dour": "Dour",
|
||||
"duplicitous": "Duplicitous",
|
||||
"effusive": "Effusive",
|
||||
"enraged": "Furioso",
|
||||
"fanatical": "Fanatical",
|
||||
"feral": "Salvaje",
|
||||
"fervent": "Fervent",
|
||||
"fickle": "Voluble",
|
||||
"fierce": "Fiero",
|
||||
"flighty": "Veleidoso",
|
||||
"flippant": "Frívolo",
|
||||
"friendly": "Amable",
|
||||
"gruff": "Hosco",
|
||||
"honorable": "Honorable",
|
||||
"hubristic": "Hubristic",
|
||||
"hungry": "Hambriento",
|
||||
"idealistic": "Idealistic",
|
||||
"imposing": "Imposing",
|
||||
"inquisitive": "Inquisitive",
|
||||
"intense": "Intenso",
|
||||
"intimidating": "Intimidante",
|
||||
"irritable": "Irritable",
|
||||
"loyal": "Leal",
|
||||
"methodical": "Methodical",
|
||||
"meticulous": "Meticulous",
|
||||
"mischievous": "Travieso",
|
||||
"moon_blessed": "Moon-blessed",
|
||||
"morose": "Taciturno",
|
||||
"near_feral": "Near feral",
|
||||
"nurturing": "Animador",
|
||||
"obsessed": "Obsessed",
|
||||
"obstinate": "Obstinado",
|
||||
"opportunistic": "Oportunista",
|
||||
"otherworldly": "Otherworldly",
|
||||
"outgoing": "Outgoing",
|
||||
"passionate": "Apasionado",
|
||||
"patient": "Patient",
|
||||
"personable": "Personable",
|
||||
"playful": "Juguetón",
|
||||
"power_hungry": "Ávido de poder",
|
||||
"proud": "Orgulloso",
|
||||
"refined": "Refined",
|
||||
"reserved": "Reserved",
|
||||
"restrained": "Contenido",
|
||||
"righteous": "Righteous",
|
||||
"scheming": "Taimado",
|
||||
"serene": "Sereno",
|
||||
"serious": "Serio",
|
||||
"shrewd": "Artero",
|
||||
"sinister": "Sinister",
|
||||
"sociable": "Sociable",
|
||||
"stoic": "Stoic",
|
||||
"starved": "Starved",
|
||||
"stubborn": "Testarudo",
|
||||
"suspicious": "Suspicaz",
|
||||
"teasing": "Bromista",
|
||||
@@ -761,7 +798,12 @@
|
||||
"uncertain": "Inseguro",
|
||||
"unenthused": "Sin entusiasmo",
|
||||
"vain": "Vanidoso",
|
||||
"wary": "Precavido"
|
||||
"vengeful": "Vengeful",
|
||||
"vindictive": "Vindictive",
|
||||
"wary": "Precavido",
|
||||
"watchful": "Watchful",
|
||||
"wrathful": "Wrathful",
|
||||
"zealous": "Zealous"
|
||||
},
|
||||
"compendium": {
|
||||
"filter_rank": "Mostrar rango",
|
||||
@@ -769,7 +811,8 @@
|
||||
"filter": {
|
||||
"rank": "Rango",
|
||||
"rarity": "Rareza",
|
||||
"ring": "Anillo"
|
||||
"ring": "Anillo",
|
||||
"clear": "Clear Filter"
|
||||
}
|
||||
},
|
||||
"source_reference": {
|
||||
@@ -799,6 +842,32 @@
|
||||
"the_scroll_or_the_blade": "El pergamino o la espada",
|
||||
"legacies_of_war": "Legacies of War",
|
||||
"children_of_the_five_winds": "Children of the Five Winds"
|
||||
},
|
||||
"tactical_grid": {
|
||||
"settings": {
|
||||
"title": "Tactical Grid Settings",
|
||||
"label": "Tactical Grid Settings",
|
||||
"hint": "Configures tactical grid range band distances (GM only) and their visual appearance colors and transparency (all users).",
|
||||
"cells": "spaces",
|
||||
"world": {
|
||||
"enabled": "Enable Tactical Grid",
|
||||
"enabled_hint": "Enables or Disable tactical grid for everyone",
|
||||
"start": "Start"
|
||||
},
|
||||
"client": {
|
||||
"color": "Color",
|
||||
"alpha": "Alpha"
|
||||
},
|
||||
"range": "Range {index}",
|
||||
"validate": {
|
||||
"start-too-small": "Must be greater than Range Band {previousRangeIndex} ({previousStart})",
|
||||
"start-too-large": "Must be lower then Range Band {nextRangeIndex} ({nextStart})"
|
||||
},
|
||||
"reset": "Reset to Default",
|
||||
"submit": "Save"
|
||||
},
|
||||
"range_band": "Range Band {band}",
|
||||
"range_abbreviation": "RB {range}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,7 @@
|
||||
"player_filter_label": "Filtre joueur",
|
||||
"player_filter_tooltip": "Applique le filtre des joueurs",
|
||||
"already_in_filter": "Filtre déjà appliqué",
|
||||
"no_results": "Aucun résultat",
|
||||
"sources_categories": {
|
||||
"rules": "Règles",
|
||||
"adventures": "Aventures",
|
||||
@@ -492,7 +493,10 @@
|
||||
"rarity_modifier": "Modificateur de rareté",
|
||||
"item_pattern": "Procédés de fabrication",
|
||||
"signature_scroll": "Rouleaux de marque",
|
||||
"school_curriculum_journal": "Déposer un journal de Cursus dans la feuille pour le lier"
|
||||
"school_curriculum_journal": "Déposer un journal de Cursus dans la feuille pour le lier",
|
||||
"warning": {
|
||||
"total_less_then_spent": "L'expérience totale est inférieure à l'expérience utilisée."
|
||||
}
|
||||
},
|
||||
"character_types": {
|
||||
"character": "Personnage Joueur",
|
||||
@@ -703,65 +707,103 @@
|
||||
"ujik": "Ujik"
|
||||
},
|
||||
"demeanor": {
|
||||
"adaptable": "Adaptable",
|
||||
"adaptable": "Malléable",
|
||||
"aggressive": "Agressive",
|
||||
"alluring": "Attirante",
|
||||
"ambitious": "Ambitieuse",
|
||||
"amiable": "Sympathique",
|
||||
"analytical": "Réfléchie",
|
||||
"angry": "Enervée",
|
||||
"amiable": "Aimable",
|
||||
"analytical": "Analytique",
|
||||
"angry": "En colère",
|
||||
"arrogant": "Arrogante",
|
||||
"assertive": "Assurée",
|
||||
"beguiling": "Séduisante",
|
||||
"assertive": "Sûre de soi",
|
||||
"beguiling": "Envoûtante",
|
||||
"bitter": "Amère",
|
||||
"bold": "Audacieuse",
|
||||
"bloodthirsty": "Sanguinaire",
|
||||
"bold": "Courageuse",
|
||||
"calculating": "Calculatrice",
|
||||
"calm": "Calme",
|
||||
"capricious": "Capricieuse",
|
||||
"cautious": "Prudente",
|
||||
"clever": "Astucieuse",
|
||||
"clever": "Malicieuse",
|
||||
"compassionate": "Compatissante",
|
||||
"confused": "Confuse",
|
||||
"courageous": "Courageuse",
|
||||
"cowardly": "Lâche",
|
||||
"crestfallen": "Démoralisée",
|
||||
"curious": "Curieuse",
|
||||
"defensive": "Sur la défensive",
|
||||
"dependable": "Fiable",
|
||||
"detached": "Détachée",
|
||||
"disheartened": "Découragée",
|
||||
"determined": "Déterminée",
|
||||
"devoted": "Fervente",
|
||||
"direct": "Directe",
|
||||
"disheartened": "Abattue",
|
||||
"dour": "Renfrognée",
|
||||
"duplicitous": "Sournoise",
|
||||
"effusive": "Communicative",
|
||||
"enraged": "Enragée",
|
||||
"fanatical": "Fanatique",
|
||||
"feral": "Sauvage",
|
||||
"fickle": "Inconstante",
|
||||
"fervent": "Dévote",
|
||||
"fickle": "Volatile",
|
||||
"fierce": "Féroce",
|
||||
"flighty": "Volage",
|
||||
"flighty": "Inconstante",
|
||||
"flippant": "Désinvolte",
|
||||
"friendly": "Amicale",
|
||||
"gruff": "Bourrue",
|
||||
"honorable": "Honorable",
|
||||
"hubristic": "Prétentieuse",
|
||||
"hungry": "Affamée",
|
||||
"intense": "Intense",
|
||||
"idealistic": "Idéaliste",
|
||||
"imposing": "Impressionnante",
|
||||
"inquisitive": "Inquisitrice",
|
||||
"intense": "Excessive",
|
||||
"intimidating": "Intimidante",
|
||||
"irritable": "Irritable",
|
||||
"loyal": "Fidèle",
|
||||
"mischievous": "Malicieuse",
|
||||
"irritable": "Colérique",
|
||||
"loyal": "Loyale",
|
||||
"methodical": "Méthodique",
|
||||
"meticulous": "Méticuleuse",
|
||||
"mischievous": "Taquine",
|
||||
"moon_blessed": "Bénie par la Lune",
|
||||
"morose": "Morose",
|
||||
"nurturing": "Encourageante",
|
||||
"near_feral": "Presque sauvage",
|
||||
"nurturing": "Maternelle",
|
||||
"obsessed": "Obsessionnelle",
|
||||
"obstinate": "Obstinée",
|
||||
"opportunistic": "Opportuniste",
|
||||
"otherworldly": "Mystique",
|
||||
"outgoing": "Agréable",
|
||||
"passionate": "Passionnée",
|
||||
"playful": "Enjouée",
|
||||
"patient": "Patiente",
|
||||
"personable": "Avenante",
|
||||
"playful": "Joueuse",
|
||||
"power_hungry": "Avide de pouvoir",
|
||||
"proud": "Fière",
|
||||
"restrained": "Restreinte",
|
||||
"scheming": "Intrigante",
|
||||
"refined": "Raffinée",
|
||||
"restrained": "Modérée",
|
||||
"reserved": "Réservée",
|
||||
"righteous": "Intègre",
|
||||
"scheming": "Fourbe",
|
||||
"serene": "Sereine",
|
||||
"serious": "Sérieuse",
|
||||
"shrewd": "Astucieuse",
|
||||
"shrewd": "Rusée",
|
||||
"sinister": "Sinistre",
|
||||
"sociable": "Affable",
|
||||
"starved": "Famélique",
|
||||
"stoic": "Stoïque",
|
||||
"stubborn": "Têtue",
|
||||
"suspicious": "Soupçonneuse",
|
||||
"teasing": "Taquine",
|
||||
"suspicious": "Suspicieuse",
|
||||
"teasing": "Moqueuse",
|
||||
"territorial": "Territoriale",
|
||||
"uncertain": "Incertaine",
|
||||
"unenthused": "Peu enthousiaste",
|
||||
"vain": "Vaine",
|
||||
"wary": "Méfiante"
|
||||
"uncertain": "Peu sûre de soi",
|
||||
"unenthused": "Amorphe",
|
||||
"vain": "Orgueilleuse",
|
||||
"vengeful": "Vengeuse",
|
||||
"vindictive": "Vindicative",
|
||||
"wary": "Méfiante",
|
||||
"watchful": "Attentif",
|
||||
"wrathful": "Furieuse",
|
||||
"zealous": "Zélée"
|
||||
},
|
||||
"compendium": {
|
||||
"filter_rank": "Aff. Rangs",
|
||||
@@ -769,7 +811,8 @@
|
||||
"filter": {
|
||||
"rank": "Rang",
|
||||
"rarity": "Rareté",
|
||||
"ring": "Anneau"
|
||||
"ring": "Anneau",
|
||||
"clear": "Suppr. les Filtres"
|
||||
}
|
||||
},
|
||||
"source_reference": {
|
||||
@@ -799,6 +842,32 @@
|
||||
"the_scroll_or_the_blade": "Le Parchemin ou le Sabre",
|
||||
"legacies_of_war": "Les Flambeaux de la Guerre",
|
||||
"children_of_the_five_winds": "Les Enfants des Cinq Vents"
|
||||
},
|
||||
"tactical_grid": {
|
||||
"settings": {
|
||||
"title": "Plan Tactique",
|
||||
"label": "Paramètres du Plan Tactique",
|
||||
"hint": "Configure les Niveaux de Portée (GM uniquement), ainsi que les différentes couleurs et transparence (tous les utilisateurs).",
|
||||
"cells": "cases",
|
||||
"world": {
|
||||
"enabled": "Activer le Plan Tactique",
|
||||
"enabled_hint": "Active ou désactive le plan tactique pour tout le monde",
|
||||
"start": "Début"
|
||||
},
|
||||
"client": {
|
||||
"color": "Couleur",
|
||||
"alpha": "Alpha"
|
||||
},
|
||||
"range": "Portée {index}",
|
||||
"validate": {
|
||||
"start-too-small": "Doit être supérieur à la Portée {previousRangeIndex} ({previousStart})",
|
||||
"start-too-large": "Doit être inférieur à la Portée {nextRangeIndex} ({nextStart})"
|
||||
},
|
||||
"reset": "Réinitialiser les paramètres par défaut",
|
||||
"submit": "Enregistrer"
|
||||
},
|
||||
"range_band": "Portée {band}",
|
||||
"range_abbreviation": "NP {range}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,7 @@
|
||||
"player_filter_label": "Player filter",
|
||||
"player_filter_tooltip": "Apply player filter",
|
||||
"already_in_filter": "Already in filter",
|
||||
"no_results": "Not Found",
|
||||
"sources_categories": {
|
||||
"rules": "Rules",
|
||||
"adventures": "Adventures",
|
||||
@@ -492,7 +493,10 @@
|
||||
"rarity_modifier": "Modificatore rarità",
|
||||
"item_pattern": "Item Patterns",
|
||||
"signature_scroll": "Signature Scrolls",
|
||||
"school_curriculum_journal": "Trascina il diario del curriculum sulla scheda per collegarlo"
|
||||
"school_curriculum_journal": "Trascina il diario del curriculum sulla scheda per collegarlo",
|
||||
"warning": {
|
||||
"total_less_then_spent": "L'esperienza totale è inferiore all'esperienza utilizzata."
|
||||
}
|
||||
},
|
||||
"character_types": {
|
||||
"character": "Personaggio giocante",
|
||||
@@ -705,6 +709,7 @@
|
||||
"demeanor": {
|
||||
"adaptable": "Flessibile",
|
||||
"aggressive": "Aggressivo",
|
||||
"alluring": "Alluring",
|
||||
"ambitious": "Ambizioso",
|
||||
"amiable": "Affabile",
|
||||
"analytical": "Analitico",
|
||||
@@ -713,6 +718,7 @@
|
||||
"assertive": "Risoluto",
|
||||
"beguiling": "Ammaliante",
|
||||
"bitter": "Amaro",
|
||||
"bloodthirsty": "Bloodthirsty",
|
||||
"bold": "Ardito",
|
||||
"calculating": "Calcolatore",
|
||||
"calm": "Calmo",
|
||||
@@ -723,37 +729,68 @@
|
||||
"confused": "Confuso",
|
||||
"courageous": "Coraggioso",
|
||||
"cowardly": "Codardo",
|
||||
"crestfallen": "Crestfallen",
|
||||
"curious": "Curioso",
|
||||
"defensive": "Defensive",
|
||||
"dependable": "Affidabile",
|
||||
"detached": "Distaccato",
|
||||
"determined": "Determined",
|
||||
"devoted": "Devoted",
|
||||
"direct": "Direct",
|
||||
"disheartened": "Sconfortato",
|
||||
"dour": "Dour",
|
||||
"duplicitous": "Duplicitous",
|
||||
"effusive": "Effusive",
|
||||
"enraged": "Infuriato",
|
||||
"fanatical": "Fanatical",
|
||||
"feral": "Selvaggio",
|
||||
"fervent": "Fervent",
|
||||
"fickle": "Volubile",
|
||||
"fierce": "Agguerrito",
|
||||
"flighty": "Volubile",
|
||||
"flippant": "Irriverente",
|
||||
"friendly": "Amichevole",
|
||||
"gruff": "Burbero",
|
||||
"honorable": "Honorable",
|
||||
"hubristic": "Prétentieuse",
|
||||
"hungry": "Affamato",
|
||||
"idealistic": "Idealistic",
|
||||
"imposing": "Imposing",
|
||||
"inquisitive": "Inquisitive",
|
||||
"intense": "Intenso",
|
||||
"intimidating": "Intimidatorio",
|
||||
"irritable": "Irritabile",
|
||||
"loyal": "Leale",
|
||||
"methodical": "Methodical",
|
||||
"meticulous": "Meticulous",
|
||||
"mischievous": "Malandrino",
|
||||
"moon_blessed": "Moon-blessed",
|
||||
"morose": "Cupo",
|
||||
"near_feral": "Near feral",
|
||||
"nurturing": "Materno",
|
||||
"obsessed": "Obsessed",
|
||||
"obstinate": "Ostinato",
|
||||
"opportunistic": "Opportunista",
|
||||
"otherworldly": "Otherworldly",
|
||||
"outgoing": "Outgoing",
|
||||
"passionate": "Appassionato",
|
||||
"patient": "Patient",
|
||||
"personable": "Personable",
|
||||
"playful": "Giocoso",
|
||||
"power_hungry": "Affamato di potere",
|
||||
"proud": "Orgoglioso",
|
||||
"refined": "Refined",
|
||||
"reserved": "Reserved",
|
||||
"restrained": "Contenuto",
|
||||
"righteous": "Righteous",
|
||||
"scheming": "Cospiratore",
|
||||
"serene": "Sereno",
|
||||
"serious": "Serio",
|
||||
"shrewd": "Scaltro",
|
||||
"sinister": "Sinister",
|
||||
"sociable": "Sociable",
|
||||
"stoic": "Stoic",
|
||||
"starved": "Starved",
|
||||
"stubborn": "Testardo",
|
||||
"suspicious": "Sospettoso",
|
||||
"teasing": "Stuzzicante",
|
||||
@@ -761,7 +798,12 @@
|
||||
"uncertain": "Incerto",
|
||||
"unenthused": "Non entusiasta",
|
||||
"vain": "Vanesio",
|
||||
"wary": "Diffidente"
|
||||
"vengeful": "Vengeful",
|
||||
"vindictive": "Vindictive",
|
||||
"wary": "Diffidente",
|
||||
"watchful": "Watchful",
|
||||
"wrathful": "Wrathful",
|
||||
"zealous": "Zealous"
|
||||
},
|
||||
"compendium": {
|
||||
"filter_rank": "Show Rank",
|
||||
@@ -769,14 +811,15 @@
|
||||
"filter": {
|
||||
"rank": "Rank",
|
||||
"rarity": "Rarity",
|
||||
"ring": "Ring"
|
||||
"ring": "Ring",
|
||||
"clear": "Clear Filter"
|
||||
}
|
||||
},
|
||||
"source_reference": {
|
||||
"core_rulebook": "Core Rulebook",
|
||||
"emerald_empire": "Emerald Empire",
|
||||
"shadowlands": "Shadowlands",
|
||||
"court_of_stones": "Court of Stones",
|
||||
"court_of_stones": "Courts of Stone",
|
||||
"path_of_waves": "Path of Waves",
|
||||
"celestial_realms": "Celestial Realms",
|
||||
"fields_of_victory": "Fields of Victory",
|
||||
@@ -799,6 +842,32 @@
|
||||
"the_scroll_or_the_blade": "The Scroll or the Blade",
|
||||
"legacies_of_war": "Legacies of War",
|
||||
"children_of_the_five_winds": "Children of the Five Winds"
|
||||
},
|
||||
"tactical_grid": {
|
||||
"settings": {
|
||||
"title": "Tactical Grid Settings",
|
||||
"label": "Tactical Grid Settings",
|
||||
"hint": "Configures tactical grid range band distances (GM only) and their visual appearance colors and transparency (all users).",
|
||||
"cells": "spaces",
|
||||
"world": {
|
||||
"enabled": "Enable Tactical Grid",
|
||||
"enabled_hint": "Enables or Disable tactical grid for everyone",
|
||||
"start": "Start"
|
||||
},
|
||||
"client": {
|
||||
"color": "Color",
|
||||
"alpha": "Alpha"
|
||||
},
|
||||
"range": "Range {index}",
|
||||
"validate": {
|
||||
"start-too-small": "Must be greater than Range Band {previousRangeIndex} ({previousStart})",
|
||||
"start-too-large": "Must be lower then Range Band {nextRangeIndex} ({nextStart})"
|
||||
},
|
||||
"reset": "Reset to Default",
|
||||
"submit": "Save"
|
||||
},
|
||||
"range_band": "Range Band {band}",
|
||||
"range_abbreviation": "RB {range}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
{"_id":"L5RCoreIte000030","name":"Tent (Yurt)","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"2","rarity":"5","zeni":"10 koku","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"245"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000031","name":"Scroll satchel","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"3","zeni":"1 bu","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"60"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000032","name":"Traveling pack","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"1","zeni":"10 koku","properties":[{"id":"L5RCorePro000012","name":"Mundane"}],"description":"","source_reference":{"source":"core_rulebook","page":"245"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000033","name":"Journal of observations","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"3","zeni":"1 bu","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"67"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000033","name":"Journal of Observations","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"3","zeni":"1 bu","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"67"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000034","name":"Omamori (Boon of Jurojin)","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"5 bu","properties":[],"description":"","source_reference":{"source":"celestial_realms","page":"97"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000035","name":"Omamori (Boon of Kisshoten)","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"5 bu","properties":[],"description":"","source_reference":{"source":"celestial_realms","page":"97"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000036","name":"Omamori (Boon of Hotei)","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"5 bu","properties":[],"description":"","source_reference":{"source":"celestial_realms","page":"97"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
@@ -119,3 +119,27 @@
|
||||
{"_id":"L5RCoreIte000121","name":"Word of the Prophet","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"9","zeni":"15 Koku","properties":[],"description":"","source_reference":{"source":"children_of_the_five_winds","page":"103"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000122","name":"Talisman of the Sun [Blessed Treasure]","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"10","zeni":"0","properties":[],"description":"","source_reference":{"source":"children_of_the_five_winds","page":"104"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000123","name":"Fox Pipe [Blessed Treasure]","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"10","zeni":"0","properties":[],"description":"","source_reference":{"source":"children_of_the_five_winds","page":"105"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000124","name":"Glass Ornament (Dragonfly)","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"6","zeni":"1 koku","properties":[],"description":"","source_reference":{"source":"writ_of_the_wild","page":"83"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000125","name":"Arrows: Armor-Piercing","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"0","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"236"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000126","name":"Arrows: Flesh-Cutter","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"0","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"236"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000127","name":"Arrows: Humming-Bulb","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"0","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"236"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000128","name":"Journal","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"1","zeni":"0","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"66"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000129","name":"Smithing hammer","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"1","zeni":"0","properties":[],"description":"","source_reference":{"source":"shadowlands","page":"89"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000130","name":"Sumai Garb","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"1","zeni":"0","properties":[],"description":"","source_reference":{"source":"fields_of_victory","page":"89"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000131","name":"Drafting Paper","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"1","zeni":"0","properties":[],"description":"","source_reference":{"source":"celestial_realms","page":"84"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000132","name":"Fine set of Chisels","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"1","zeni":"0","properties":[],"description":"","source_reference":{"source":"celestial_realms","page":"84"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000133","name":"Omamori (Boon of Fukurokujin)","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"5 bu","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"243"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000134","name":"Omamori (Boon of Bishamon)","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"5 bu","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"243"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000135","name":"Omamori (Boon of Benten)","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"5 bu","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"243"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000136","name":"Poison - Noxious Poison (One Vial)","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"5","zeni":"30 zeni","properties":[{"id":"L5RCorePro000009","name":"Forbidden"}],"description":"","source_reference":{"source":"core_rulebook","page":"244"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000137","name":"Poison - Fire Biter (One Vial)","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"5","zeni":"30 zeni","properties":[{"id":"L5RCorePro000009","name":"Forbidden"}],"description":"","source_reference":{"source":"core_rulebook","page":"244"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000138","name":"Poison - Night Milk (One Vial)","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"5","zeni":"30 zeni","properties":[{"id":"L5RCorePro000009","name":"Forbidden"}],"description":"","source_reference":{"source":"core_rulebook","page":"244"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000139","name":"Blanket","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"0","zeni":"1 zeni","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"245"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000140","name":"Bowl","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"0","zeni":"1 zeni","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"245"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000141","name":"Flint and Tinder","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"0","zeni":"1 zeni","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"245"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000142","name":"Furoshiki","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"0","zeni":"1 zeni","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"245"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000143","name":"The Obsidian Journal","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"10","zeni":"0","properties":[{"id":"L5RCorePro000009","name":"Forbidden"},{"id":"L5RCorePro000008","name":"Unholy"}],"description":"","source_reference":{"source":"core_rulebook","page":"127"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000144","name":"Pouch of Insence","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"0","properties":[],"description":"","source_reference":{"source":"celestial_realms","page":"85"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000145","name":"Religious texts","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"0","properties":[],"description":"","source_reference":{"source":"celestial_realms","page":"86"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000146","name":"Small Sachel of Ingredients","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"10 bu","properties":[],"description":"","source_reference":{"source":"celestial_realms","page":"80"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
{"_id":"L5RCoreIte000147","name":"Blessed Glass vial","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"0","properties":[],"description":"","source_reference":{"source":"celestial_realms","page":"80"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
|
||||
|
||||
@@ -73,7 +73,7 @@ export class ActorL5r5e extends Actor {
|
||||
);
|
||||
break;
|
||||
}
|
||||
await super.create(docData, options);
|
||||
return super.create(docData, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -624,6 +624,36 @@ export class BaseCharacterSheetL5r5e extends BaseSheetL5r5e {
|
||||
});
|
||||
break;
|
||||
|
||||
case "honor":
|
||||
await this.actor.update({
|
||||
system: {
|
||||
social: {
|
||||
honor: Math.max(0, this.actor.system.social.honor + mod),
|
||||
},
|
||||
},
|
||||
});
|
||||
break;
|
||||
|
||||
case "glory":
|
||||
await this.actor.update({
|
||||
system: {
|
||||
social: {
|
||||
glory: Math.max(0, this.actor.system.social.glory + mod),
|
||||
},
|
||||
},
|
||||
});
|
||||
break;
|
||||
|
||||
case "status":
|
||||
await this.actor.update({
|
||||
system: {
|
||||
social: {
|
||||
status: Math.max(0, this.actor.system.social.status + mod),
|
||||
},
|
||||
},
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn("L5R5E | BCS | Unsupported type", type);
|
||||
break;
|
||||
|
||||
@@ -56,6 +56,9 @@ export class CharacterSheetL5r5e extends BaseCharacterSheetL5r5e {
|
||||
// Split Others advancements, and calculate xp spent and add it to total
|
||||
this._prepareOthersAdvancement(sheetData);
|
||||
|
||||
// Update spent_xp to actor
|
||||
this.actor.system.xp_spent = sheetData.data.system.xp_spent;
|
||||
|
||||
// Total
|
||||
sheetData.data.system.xp_saved = Math.floor(
|
||||
parseInt(sheetData.data.system.xp_total) - parseInt(sheetData.data.system.xp_spent)
|
||||
@@ -114,6 +117,9 @@ export class CharacterSheetL5r5e extends BaseCharacterSheetL5r5e {
|
||||
// Money +/-
|
||||
html.find(".money-control").on("click", this._modifyMoney.bind(this));
|
||||
|
||||
// XP +/-
|
||||
html.find(".xp-control").on("click", this._modifyXP.bind(this));
|
||||
|
||||
// Advancements Tab to current rank onload
|
||||
// TODO class "Active" Bug on load, dunno why :/
|
||||
this._tabs
|
||||
@@ -149,6 +155,12 @@ export class CharacterSheetL5r5e extends BaseCharacterSheetL5r5e {
|
||||
adv[rank].spent.total += xp_used_total;
|
||||
adv[rank].spent.curriculum += xp_used;
|
||||
});
|
||||
|
||||
// If we finished the rank but haven't added anything to the next rank we should show an empty tab
|
||||
// note: adv is index from 1, not 0 because of rank starting at 1
|
||||
if(adv.length -1 < sheetData.data.system.identity.school_rank) {
|
||||
adv.push({list: [], rank: sheetData.data.system.identity.school_rank, spent: { total: 0, curriculum: 0}});
|
||||
}
|
||||
sheetData.data.advancementsListByRank = adv;
|
||||
}
|
||||
|
||||
@@ -285,6 +297,35 @@ export class CharacterSheetL5r5e extends BaseCharacterSheetL5r5e {
|
||||
this.render(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or Subtract XP (+/- buttons)
|
||||
* @param {Event} event
|
||||
* @private
|
||||
*/
|
||||
async _modifyXP(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const elmt = $(event.currentTarget);
|
||||
let mod = elmt.data("value");
|
||||
if (!mod) {
|
||||
return;
|
||||
}
|
||||
|
||||
const new_xp_total = Math.max(0, this.actor.system.xp_total + mod);
|
||||
this.actor.update({
|
||||
system: {
|
||||
xp_total: new_xp_total,
|
||||
},
|
||||
});
|
||||
|
||||
if(this.actor.system.xp_spent > new_xp_total) {
|
||||
ui.notifications.warn("l5r5e.advancements.warning.total_less_then_spent", { localize: true })
|
||||
}
|
||||
|
||||
this.render(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add +1 to actor school rank
|
||||
* @param {Event} event
|
||||
|
||||
@@ -111,7 +111,7 @@ export class CombatL5r5e extends Combat {
|
||||
if (messageOptions.rnkRoll instanceof game.l5r5e.RollL5r5e && ids.length === 1) {
|
||||
// Specific RnK
|
||||
roll = messageOptions.rnkRoll;
|
||||
rnkMessage = await roll.toMessage({ flavor }, { rollMode: messageOptions.rollMode || null });
|
||||
rnkMessage = await roll.toMessage({ flavor }, { messageMode: messageOptions.messageMode || null });
|
||||
} else {
|
||||
// Regular
|
||||
roll = new game.l5r5e.RollL5r5e(formula ?? createFormula.join("+"));
|
||||
|
||||
11
system/scripts/compendium/l5r5e-compendium-directory.js
Normal file
11
system/scripts/compendium/l5r5e-compendium-directory.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const { CompendiumDirectory } = foundry.applications.sidebar.tabs;
|
||||
|
||||
export class CompendiumDirectoryL5r5e extends CompendiumDirectory {
|
||||
|
||||
/** @inheritdoc */
|
||||
async _prepareContext(options) {
|
||||
const context = await super._prepareContext(options);
|
||||
context.sidebarIcon = foundry.applications.sidebar.Sidebar.TABS.compendium.icon;
|
||||
return context;
|
||||
}
|
||||
}
|
||||
497
system/scripts/compendium/l5r5e-item-compendium.js
Normal file
497
system/scripts/compendium/l5r5e-item-compendium.js
Normal file
@@ -0,0 +1,497 @@
|
||||
import { L5r5eHtmlMultiSelectElement } from "../misc/l5r5e-multiselect.js";
|
||||
|
||||
const { Compendium } = foundry.applications.sidebar.apps;
|
||||
|
||||
/**
|
||||
* Extended Compendium application for L5R5e.
|
||||
* Adds source/rank/ring/rarity filters to Item compendiums
|
||||
* @extends {Compendium}
|
||||
*/
|
||||
export class ItemCompendiumL5r5e extends Compendium {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
actions: {
|
||||
applyPlayerView: ItemCompendiumL5r5e.#onApplyPlayerView,
|
||||
},
|
||||
window: {
|
||||
resizable: true
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Our own entry partial which mirrors Foundry's index-partial.hbs structure
|
||||
* and appends ring/rarity/rank badges using data from _prepareDirectoryContext.
|
||||
*
|
||||
* NOTE: We intentionally duplicate Foundry's <li> structure here rather than
|
||||
* trying to include their partial, because their partial renders a complete <li>
|
||||
* element which cannot be nested or extended from outside. If Foundry ever
|
||||
* changes their index-partial.hbs, this file will need updating to match.
|
||||
* @override
|
||||
*/
|
||||
static _entryPartial = "systems/l5r5e/templates/" + "compendium/l5r5e-index-partial.html";
|
||||
|
||||
/**
|
||||
* Sources present in this specific compendium, populated during _prepareContext.
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
#sourcesInThisCompendium = new Set();
|
||||
|
||||
/**
|
||||
* Sources unavailable to players based on permission settings.
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
#unavailableSourceForPlayersSet = new Set();
|
||||
|
||||
/**
|
||||
* Whether to hide entries with empty sources from players.
|
||||
* @type {boolean}
|
||||
*/
|
||||
#hideEmptySourcesFromPlayers = false;
|
||||
|
||||
/**
|
||||
* Which filter UI controls are worth showing.
|
||||
* Determined during _prepareContext by checking whether at least two
|
||||
* distinct values exist for each filterable property.
|
||||
* @type {{ rank: boolean, rarity: boolean, source: boolean, ring: boolean }|null}
|
||||
*/
|
||||
#filtersToShow = null;
|
||||
|
||||
/**
|
||||
* Cached active filter values, read from the DOM once at the start of
|
||||
* each filter pass in #reapplyFilters and held for _onMatchSearchEntry
|
||||
* to consume per-entry without re-querying the DOM.
|
||||
* @type {{ userFilter: string[], rankFilter: string[], ringFilter: string[], rarityFilter: string[] }}
|
||||
*/
|
||||
#activeFilters = {
|
||||
userFilter: [],
|
||||
rankFilter: [],
|
||||
ringFilter: [],
|
||||
rarityFilter: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Insert the filter part between header and directory by composing with
|
||||
* the parent parts rather than replacing them, so future Foundry changes
|
||||
* to Compendium.PARTS are picked up automatically.
|
||||
* @override
|
||||
*/
|
||||
_configureRenderParts(options) {
|
||||
const parts = super._configureRenderParts(options);
|
||||
|
||||
const ordered = {};
|
||||
for (const [key, value] of Object.entries(parts)) {
|
||||
ordered[key] = value;
|
||||
if (key === "header") {
|
||||
ordered.filter = {
|
||||
template: `${CONFIG.l5r5e.paths.templates}compendium/filter-bar.html`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return ordered;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _prepareContext(options) {
|
||||
const context = await super._prepareContext(options);
|
||||
this.#sourcesInThisCompendium = new Set();
|
||||
this.#resolvePermissions();
|
||||
this.#filtersToShow = this.#computeFilterVisibility();
|
||||
return context;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _preparePartContext(partId, context, options) {
|
||||
context = await super._preparePartContext(partId, context, options);
|
||||
|
||||
if (partId === "filter") {
|
||||
const ns = CONFIG.l5r5e.namespace;
|
||||
const allCompendiumReferencesSet = game.settings.get(ns, "all-compendium-references");
|
||||
const hideDisabledOptions = game.settings.get(ns, "compendium-hide-disabled-sources");
|
||||
|
||||
context.filtersToShow = this.#filtersToShow;
|
||||
context.ranks = [1, 2, 3, 4, 5];
|
||||
context.rarities = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
context.rings = ["fire", "water", "earth", "air", "void"];
|
||||
context.hideDisabledOptions = hideDisabledOptions;
|
||||
context.showPlayerView = game.user.isGM && this.#unavailableSourceForPlayersSet.size > 0;
|
||||
|
||||
// Source multiselect options — plain data for {{selectOptions}} in the template.
|
||||
context.sources = [...allCompendiumReferencesSet].map((reference) => ({
|
||||
value: reference,
|
||||
label: CONFIG.l5r5e.sourceReference[reference]?.label ?? reference,
|
||||
translate: true,
|
||||
group:
|
||||
CONFIG.l5r5e.sourceReference[reference]?.type.split(",")[0] ??
|
||||
"l5r5e.multiselect.sources_categories.others",
|
||||
disabled:
|
||||
!this.#sourcesInThisCompendium.has(reference) ||
|
||||
(!game.user.isGM && this.#unavailableSourceForPlayersSet.has(reference)),
|
||||
}));
|
||||
}
|
||||
|
||||
if (partId === "directory") {
|
||||
context.entryFilterData = Object.fromEntries(
|
||||
[...this.collection.index.values()].map((entry) => [
|
||||
entry._id,
|
||||
{
|
||||
rank: entry.system?.rank,
|
||||
ring: entry.system?.ring,
|
||||
rarity: entry.system?.rarity,
|
||||
},
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _onRender(context, options) {
|
||||
await super._onRender(context, options);
|
||||
|
||||
if (options.parts.includes("filter")) {
|
||||
this.#bindButtonFilter(".rank-filter");
|
||||
this.#bindButtonFilter(".rarity-filter");
|
||||
this.#bindButtonFilter(".ring-filter");
|
||||
this.#bindSourceFilter();
|
||||
}
|
||||
|
||||
// Reapply filters whenever the filter controls or the entry list changes.
|
||||
if (options.parts.some((p) => p === "filter" || p === "directory")) {
|
||||
this.#reapplyFilters();
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_preSyncPartState(partId, newElement, priorElement, state) {
|
||||
super._preSyncPartState(partId, newElement, priorElement, state);
|
||||
|
||||
if (partId === "filter") {
|
||||
state.selectedRanks = [...priorElement.querySelectorAll(".rank-filter .selected")].map((element) => element.dataset.rank);
|
||||
state.selectedRarities = [...priorElement.querySelectorAll(".rarity-filter .selected")].map((element) => element.dataset.rarity);
|
||||
state.selectedRings = [...priorElement.querySelectorAll(".ring-filter .selected")].map((element) => element.dataset.ring);
|
||||
state.sourceValue = priorElement.querySelector("l5r5e-multi-select")?.value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore filter selections after the filter part has been re-rendered.
|
||||
* The [data-clear] button visibility is derived from whether any values
|
||||
* were restored — no extra state needed.
|
||||
* @override
|
||||
*/
|
||||
_syncPartState(partId, newElement, priorElement, state) {
|
||||
super._syncPartState(partId, newElement, priorElement, state);
|
||||
|
||||
if (partId === "filter") {
|
||||
for (const rank of state.selectedRanks ?? []) {
|
||||
newElement.querySelector(`.rank-filter [data-rank="${rank}"]`)?.classList.add("selected");
|
||||
}
|
||||
const rankClear = newElement.querySelector(".rank-filter [data-clear]");
|
||||
if (rankClear) {
|
||||
rankClear.style.display = state.selectedRanks?.length ? "" : "none";
|
||||
}
|
||||
|
||||
for (const rarity of state.selectedRarities ?? []) {
|
||||
newElement.querySelector(`.rarity-filter [data-rarity="${rarity}"]`)?.classList.add("selected");
|
||||
}
|
||||
const rarityClear = newElement.querySelector(".rarity-filter [data-clear]");
|
||||
if (rarityClear) {
|
||||
rarityClear.style.display = state.selectedRarities?.length ? "" : "none";
|
||||
}
|
||||
|
||||
for (const ring of state.selectedRings ?? []) {
|
||||
newElement.querySelector(`.ring-filter [data-ring="${ring}"]`)?.classList.add("selected");
|
||||
}
|
||||
const ringClear = newElement.querySelector(".ring-filter [data-clear]");
|
||||
if (ringClear) {
|
||||
ringClear.style.display = state.selectedRings?.length ? "" : "none";
|
||||
}
|
||||
|
||||
if (state.sourceValue) {
|
||||
const multiSelect = newElement.querySelector("l5r5e-multi-select");
|
||||
if (multiSelect) {
|
||||
multiSelect.value = state.sourceValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_onMatchSearchEntry(query, entryIds, entry, options) {
|
||||
super._onMatchSearchEntry(query, entryIds, entry, options);
|
||||
if (entry.style.display === "none") {
|
||||
return;
|
||||
}
|
||||
this.#applyEntryFilter(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshot active filter state then re-run the search filter (or walk entries directly as fallback).
|
||||
* @private
|
||||
*/
|
||||
#reapplyFilters() {
|
||||
this.#refreshActiveFilters();
|
||||
|
||||
const searchFilter = this._searchFilters?.[0];
|
||||
if (searchFilter) {
|
||||
searchFilter.filter(null, searchFilter.query);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback
|
||||
for (const entry of this.element.querySelectorAll(".directory-item")) {
|
||||
this.#applyEntryFilter(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read current filter selections from the DOM and cache them in #activeFilters.
|
||||
* @private
|
||||
*/
|
||||
#refreshActiveFilters() {
|
||||
const filterElement = this.element.querySelector("[data-application-part=\"filter\"]");
|
||||
const multiSelect = filterElement?.querySelector("l5r5e-multi-select");
|
||||
|
||||
const collectSelected = (containerSelector, dataKey) =>
|
||||
[...(filterElement?.querySelectorAll(`${containerSelector} .selected`) ?? [])]
|
||||
.map((element) => element.dataset[dataKey])
|
||||
.filter(Boolean);
|
||||
|
||||
this.#activeFilters = {
|
||||
userFilter: multiSelect?.value ?? [],
|
||||
rankFilter: collectSelected(".rank-filter", "rank"),
|
||||
ringFilter: collectSelected(".ring-filter", "ring"),
|
||||
rarityFilter: collectSelected(".rarity-filter", "rarity"),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply all active filters to a single directory entry, showing or hiding it accordingly.
|
||||
* @param {HTMLElement} entry
|
||||
* @private
|
||||
*/
|
||||
#applyEntryFilter(entry) {
|
||||
const indexEntry = this.collection.index.get(entry.dataset.entryId);
|
||||
if (!indexEntry) {
|
||||
return;
|
||||
}
|
||||
|
||||
const system = indexEntry.system;
|
||||
const lineSource = system?.source_reference?.source ?? null;
|
||||
const { userFilter, rankFilter, ringFilter, rarityFilter } = this.#activeFilters;
|
||||
let shouldShow = true;
|
||||
|
||||
const sourceUnavailable =
|
||||
(lineSource && this.#unavailableSourceForPlayersSet.has(lineSource)) ||
|
||||
(lineSource === "" && this.#hideEmptySourcesFromPlayers);
|
||||
|
||||
if (sourceUnavailable) {
|
||||
if (game.user.isGM) {
|
||||
entry.classList.add("not-for-players");
|
||||
entry.dataset.tooltip = game.i18n.localize("l5r5e.compendium.not_for_players");
|
||||
} else {
|
||||
shouldShow = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (rankFilter.length) {
|
||||
shouldShow &&= rankFilter.includes(String(system?.rank));
|
||||
}
|
||||
if (rarityFilter.length) {
|
||||
shouldShow &&= rarityFilter.includes(String(system?.rarity));
|
||||
}
|
||||
if (ringFilter.length) {
|
||||
shouldShow &&= ringFilter.includes(system?.ring);
|
||||
}
|
||||
if (userFilter.length) {
|
||||
shouldShow &&= userFilter.includes(lineSource);
|
||||
}
|
||||
|
||||
entry.style.display = shouldShow ? "" : "none";
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate the compendium index to:
|
||||
* 1. Populate #sourcesInThisCompendium for source filter options
|
||||
* 2. Determine which filter controls have enough distinct values to show
|
||||
* @returns {{ rank: boolean, rarity: boolean, source: boolean, ring: boolean }}
|
||||
* @private
|
||||
*/
|
||||
#computeFilterVisibility() {
|
||||
const filtersToShow = { rank: false, rarity: false, source: true, ring: false };
|
||||
const firstSeen = { rank: null, rarity: null, ring: null };
|
||||
|
||||
const markIfDistinct = (prop, value) => {
|
||||
if (filtersToShow[prop] || value === undefined || value === null) {
|
||||
return;
|
||||
}
|
||||
if (firstSeen[prop] === null) {
|
||||
firstSeen[prop] = value;
|
||||
} else if (firstSeen[prop] !== value) {
|
||||
filtersToShow[prop] = true;
|
||||
}
|
||||
};
|
||||
|
||||
for (const entry of this.collection.index.values()) {
|
||||
const sys = entry.system;
|
||||
if (!sys) {
|
||||
continue;
|
||||
}
|
||||
if (sys.rank !== undefined) {
|
||||
markIfDistinct("rank", sys.rank);
|
||||
}
|
||||
if (sys.ring !== undefined) {
|
||||
markIfDistinct("ring", sys.ring);
|
||||
}
|
||||
if (sys.rarity !== undefined) {
|
||||
markIfDistinct("rarity", sys.rarity);
|
||||
}
|
||||
if (sys.source_reference?.source !== undefined) {
|
||||
this.#sourcesInThisCompendium.add(sys.source_reference.source);
|
||||
}
|
||||
}
|
||||
|
||||
return filtersToShow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve which sources are restricted from players and cache the result
|
||||
* in instance-level sets for use by #applyEntryFilter.
|
||||
* @private
|
||||
*/
|
||||
#resolvePermissions() {
|
||||
const ns = CONFIG.l5r5e.namespace;
|
||||
const officialSet = game.settings.get(ns, "compendium-official-content-for-players");
|
||||
const unofficialSet = game.settings.get(ns, "compendium-unofficial-content-for-players");
|
||||
const allRefsSet = game.settings.get(ns, "all-compendium-references");
|
||||
|
||||
this.#hideEmptySourcesFromPlayers = game.settings.get(ns, "compendium-hide-empty-sources-from-players");
|
||||
|
||||
this.#unavailableSourceForPlayersSet = new Set(
|
||||
[...allRefsSet].filter((ref) => {
|
||||
if (CONFIG.l5r5e.sourceReference[ref]) {
|
||||
return officialSet.size > 0 ? !officialSet.has(ref) : false;
|
||||
}
|
||||
return unofficialSet.size > 0 ? !unofficialSet.has(ref) : false;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind toggle-selection click handlers to all children of a button filter container.
|
||||
* A [data-clear] element at the end of the container acts as an inline reset:
|
||||
* - It is hidden (display:none in the template) when no values are selected.
|
||||
* - It becomes visible as soon as any value is selected.
|
||||
* - Clicking it deselects all values and hides itself again.
|
||||
* @param {string} containerSelector
|
||||
* @private
|
||||
*/
|
||||
#bindButtonFilter(containerSelector) {
|
||||
const container = this.element.querySelector(containerSelector);
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clearButton = container.querySelector("[data-clear]");
|
||||
|
||||
const updateClearButton = () => {
|
||||
if (!clearButton) {
|
||||
return;
|
||||
}
|
||||
const anySelected = [...container.children].some(
|
||||
(element) => element.dataset.clear === undefined && element.classList.contains("selected")
|
||||
);
|
||||
clearButton.style.display = anySelected ? "" : "none";
|
||||
};
|
||||
|
||||
for (const child of container.children) {
|
||||
child.addEventListener("click", (event) => {
|
||||
const target = event.currentTarget;
|
||||
|
||||
if (target.dataset.clear !== undefined) {
|
||||
// Clicked the clear button — deselect all value elements
|
||||
for (const element of container.children) {
|
||||
element.classList.remove("selected");
|
||||
}
|
||||
} else {
|
||||
// Clicked a value element — toggle it
|
||||
target.classList.toggle("selected");
|
||||
}
|
||||
|
||||
updateClearButton();
|
||||
this.#reapplyFilters();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire up the change listener on the already-rendered multiselect element.
|
||||
* The element and its options are fully declared in filter-bar.html via
|
||||
* {{selectOptions}} — no imperative construction needed here.
|
||||
* @private
|
||||
*/
|
||||
#bindSourceFilter() {
|
||||
const multiSelect = this.element.querySelector("l5r5e-multi-select[name=\"filter-sources\"]");
|
||||
if (!multiSelect) {
|
||||
return;
|
||||
}
|
||||
multiSelect.addEventListener("change", () => this.#reapplyFilters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the GM player-view button, selecting only the sources that are
|
||||
* both visible to players and present in this specific compendium.
|
||||
* @this {ItemCompendiumL5r5e}
|
||||
* @private
|
||||
*/
|
||||
static #onApplyPlayerView() {
|
||||
const ns = CONFIG.l5r5e.namespace;
|
||||
const allRefsSet = game.settings.get(ns, "all-compendium-references");
|
||||
|
||||
const availableForPlayers = [...allRefsSet]
|
||||
.filter((ref) => !this.#unavailableSourceForPlayersSet.has(ref))
|
||||
.filter((ref) => this.#sourcesInThisCompendium.has(ref));
|
||||
|
||||
const multiSelect = this.element.querySelector("l5r5e-multi-select[name=\"filter-sources\"]");
|
||||
if (!multiSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
multiSelect.value = availableForPlayers;
|
||||
this.#reapplyFilters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register this compendium class and extend the index fields for all Item packs.
|
||||
*/
|
||||
static applyToPacks() {
|
||||
CONFIG.Item.compendiumIndexFields = [
|
||||
...(CONFIG.Item.compendiumIndexFields ?? []),
|
||||
"system.rank",
|
||||
"system.ring",
|
||||
"system.rarity",
|
||||
"system.source_reference.source",
|
||||
];
|
||||
|
||||
for (const pack of game.packs.filter((p) => p.metadata.type === "Item")) {
|
||||
pack.applicationClass = ItemCompendiumL5r5e;
|
||||
pack.getIndex(); // rebuild index with new fields — no need to await since this happens before anyone have a chance to act
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,6 +180,10 @@ export const L5R5E = {
|
||||
name: "l5r5e.conditions.unconscious",
|
||||
img: "systems/l5r5e/assets/icons/conditions/unconscious.webp",
|
||||
system: { id: "L5RCoreCon000015" }
|
||||
},{
|
||||
id: "dead",
|
||||
name: "EFFECT.StatusDead",
|
||||
img: "icons/svg/skull.svg"
|
||||
}],
|
||||
regex: {
|
||||
techniqueDifficulty: /^@([TS]):([^|]+?)(?:\|(min|max)(?:\(([^)]+?)\))?)?$/,
|
||||
@@ -456,6 +460,7 @@ L5R5E.demeanors = [
|
||||
{ id: "adaptable", mod: { water: 2, earth: -2 } },
|
||||
{ id: "aggressive", mod: { fire: 2, air: -2 } },
|
||||
{ id: "aggressive", mod: { fire: 2, water: -2 } },
|
||||
{ id: "alluring", mod: { air: 2, earth: -1, fire: -1 } },
|
||||
{ id: "ambitious", mod: { fire: 2, water: -2 } },
|
||||
{ id: "amiable", mod: { air: 2, earth: -2 } },
|
||||
{ id: "analytical", mod: { fire: 2, air: -2 } },
|
||||
@@ -466,23 +471,38 @@ L5R5E.demeanors = [
|
||||
{ id: "beguiling", mod: { air: 2, earth: -2 } },
|
||||
{ id: "beguiling", mod: { fire: 2, earth: -2 } },
|
||||
{ id: "bitter", mod: { fire: 2, air: -2 } },
|
||||
{ id: "bloodthirsty", mod: { fire: 2, water: -2 } },
|
||||
{ id: "bold", mod: { fire: 1, earth: -1 } },
|
||||
{ id: "calculating", mod: { air: 2, fire: -2 } },
|
||||
{ id: "calm", mod: { fire: 2, air: -2 } },
|
||||
{ id: "capricious", mod: { air: 2, earth: -2 } },
|
||||
{ id: "cautious", mod: { air: 2, earth: -2 } },
|
||||
{ id: "cautious", mod: { water: 1, void: -1 } },
|
||||
{ id: "clever", mod: { air: 2, earth: -2 } },
|
||||
{ id: "compassionate", mod: { fire: 2, air: -1, water: -1}},
|
||||
{ id: "compassionate", mod: { water: 2, fire: -2 } },
|
||||
{ id: "compassionate", mod: { water: 2, void: -2 } },
|
||||
{ id: "confused", mod: { fire: 1, void: 1, air: -2 } },
|
||||
{ id: "courageous", mod: { air: 2, earth: -2 } },
|
||||
{ id: "cowardly", mod: { earth: 2, fire: -2 } },
|
||||
{ id: "crestfallen", mod: { void: 2, fire: -2 } },
|
||||
{ id: "curious", mod: { earth: 1, void: -2 } },
|
||||
{ id: "curious", mod: { fire: 1, void: 1, air: -2 } },
|
||||
{ id: "defensive", mod: { fire: 2, air: -2 } },
|
||||
{ id: "dependable", mod: { fire: 1, water: 1, earth: -2 } },
|
||||
{ id: "detached", mod: { earth: 1, fire: 1, void: -2 } },
|
||||
{ id: "determined", mod: { earth: 2, air: -2 } },
|
||||
{ id: "devoted", mod: { fire: 2, earth: -2 } },
|
||||
{ id: "direct", mod: { air: 2, fire: -1, water: -1 } },
|
||||
{ id: "disheartened", mod: { fire: 1, earth: -1 } },
|
||||
{ id: "dour", mod: { earth: 1, water: 1, air: -1 } },
|
||||
{ id: "duplicitous", mod: { water: 2, fire: -2 } },
|
||||
{ id: "effusive", mod: { air: 2, earth: -2 } },
|
||||
{ id: "enraged", mod: { air: 1, fire: -2 } },
|
||||
{ id: "fanatical", mod: { earth: 1, air: 1, fire: -2 } },
|
||||
{ id: "feral", mod: { air: 2, fire: -2 } },
|
||||
{ id: "fervent", mod: { fire: 2, earth: -2 } },
|
||||
{ id: "fervent", mod: { air: 1, water: 1, fire: -1, void: -1 } },
|
||||
{ id: "fickle", mod: { fire: 2, air: -2 } },
|
||||
{ id: "fierce", mod: { fire: 2, earth: -2 } },
|
||||
{ id: "flighty", mod: { air: 2, fire: -2 } },
|
||||
@@ -490,32 +510,55 @@ L5R5E.demeanors = [
|
||||
{ id: "flippant", mod: { fire: 2, air: -2 } },
|
||||
{ id: "friendly", mod: { fire: 1, earth: -2, water: -2 } },
|
||||
{ id: "gruff", mod: { water: 2, earth: -2 } },
|
||||
{ id: "honorable", mod: { fire: 2, earth: -2 } },
|
||||
{ id: "hubristic", mod: { earth: 2, air: -2 } },
|
||||
{ id: "hungry", mod: { fire: 2, air: -2 } },
|
||||
{ id: "idealistic", mod: { water: 2, earth: -2 } },
|
||||
{ id: "idealistic", mod: { earth: 1, water: -1 } },
|
||||
{ id: "imposing", mod: { fire: 2, water: -2 } },
|
||||
{ id: "inquisitive", mod: { earth: 2, water: -2 } },
|
||||
{ id: "intense", mod: { air: 2, water: -2 } },
|
||||
{ id: "intense", mod: { fire: 2, water: -2 } },
|
||||
{ id: "intimidating", mod: { fire: 2, air: -2 } },
|
||||
{ id: "irritable", mod: { fire: 2, air: -1, water: -1 } },
|
||||
{ id: "loyal", mod: { air: 1, earth: -2, fire: -2 } },
|
||||
{ id: "loyal", mod: { water: 2, fire: -2 } },
|
||||
{ id: "methodical", mod: { earth: 2, fire: -2 } },
|
||||
{ id: "meticulous", mod: { fire: 1, water: 1, air: -1, earth: -1 } },
|
||||
{ id: "meticulous", mod: { fire: 1, water: 1, earth: -2 } },
|
||||
{ id: "mischievous", mod: { fire: 2, air: -2 } },
|
||||
{ id: "mischievous", mod: { air: 2, earth: -2 } },
|
||||
{ id: "mischievous", mod: { earth: 2, fire: -2 } },
|
||||
{ id: "moon_blessed", mod: { water: 2, fire: -2 } },
|
||||
{ id: "morose", mod: { water: 2, fire: -2 } },
|
||||
{ id: "near_feral", mod: { air: 1, fire: -1 } },
|
||||
{ id: "nurturing", mod: { earth: 2, fire: -2 } },
|
||||
{ id: "obsessed", mod: { earth: 2, air: -2 } },
|
||||
{ id: "obstinate", mod: { earth: 2, air: -2 } },
|
||||
{ id: "obstinate", mod: { water: 2, air: -2 } },
|
||||
{ id: "otherworldly", mod: { water: 1, void: -1 } },
|
||||
{ id: "outgoing", mod: { air: 2, earth: -2 } },
|
||||
{ id: "opportunistic", mod: { water: 2, fire: -2 } },
|
||||
{ id: "passionate", mod: { earth: 2, air: -2 } },
|
||||
{ id: "patient", mod: { fire: 1, water: 1, air: -1, void: -1 } },
|
||||
{ id: "personable", mod: { fire: 2, air: 1, void: -2 } },
|
||||
{ id: "playful", mod: { earth: 2, water: -2 } },
|
||||
{ id: "playful", mod: { fire: 1, air: 1, void: -2 } },
|
||||
{ id: "power_hungry", mod: { fire: 2, earth: -2 } },
|
||||
{ id: "proud", mod: { fire: 2, earth: -2 } },
|
||||
{ id: "refined", mod: { earth: 1, water: 1, air: -1, fire: -1 } },
|
||||
{ id: "reserved", mod: { earth: 2, water: -2 } },
|
||||
{ id: "restrained", mod: { earth: 2, air: -2 } },
|
||||
{ id: "righteous", mod: { water: 2, fire: -1, void: -1 } },
|
||||
{ id: "scheming", mod: { air: 2, void: -2 } },
|
||||
{ id: "serene", mod: { fire: 2, void: -2 } },
|
||||
{ id: "serene", mod: { void: 2, fire: -2 } },
|
||||
{ id: "serious", mod: { fire: 2, earth: -2 } },
|
||||
{ id: "shrewd", mod: { air: 2, fire: -2 } },
|
||||
{ id: "sinister", mod: { fire: 2, air: -2 } },
|
||||
{ id: "sociable", mod: { air: 1, earth: 1, fire: -1, water: -1 } },
|
||||
{ id: "starved", mod: { water: 2, fire: -2 } },
|
||||
{ id: "stoic", mod: { earth: 2, fire: -2 } },
|
||||
{ id: "stubborn", mod: { earth: 2, water: -2 } },
|
||||
{ id: "suspicious", mod: { air: 2, earth: -2 } },
|
||||
{ id: "teasing", mod: { air: 2, earth: -2 } },
|
||||
@@ -523,5 +566,10 @@ L5R5E.demeanors = [
|
||||
{ id: "uncertain", mod: { air: 2, fire: -2 } },
|
||||
{ id: "unenthused", mod: { earth: 2, fire: -2 } },
|
||||
{ id: "vain", mod: { earth: 2, air: -2 } },
|
||||
{ id: "vengeful", mod: { fire: 2, void: -2 } },
|
||||
{ id: "vindictive", mod: { fire: 2, water: -2 } },
|
||||
{ id: "wary", mod: { earth: 2, fire: -2 } },
|
||||
{ id: "watchful", mod: { fire: 2, earth: -1, void: -1 } },
|
||||
{ id: "wrathful", mod: { fire: 2, earth: -2 } },
|
||||
{ id: "zealous", mod: { earth: 2, fire: -2 } },
|
||||
];
|
||||
|
||||
@@ -1,47 +1,110 @@
|
||||
import { L5r5eHtmlMultiSelectElement } from "../misc/l5r5e-multiselect.js";
|
||||
|
||||
/**
|
||||
* A subclass of [ArrayField]{@link ArrayField} which supports a set of contained elements.
|
||||
* Elements in this set are treated as fungible and may be represented in any order or discarded if invalid.
|
||||
* A Foundry `SetField` that renders as an {@link L5r5eHtmlMultiSelectElement} chip-input.
|
||||
*
|
||||
* Use this in a DataModel schema whenever a field stores an unordered collection of
|
||||
* string values drawn from a fixed option list. On form submission the element returns a
|
||||
* comma-separated string; `clean()` splits it back into an Array before Foundry processes
|
||||
* it, and `initialize()` wraps the result in a `Set` for use in the model.
|
||||
*
|
||||
* @example
|
||||
* // In a DataModel schema:
|
||||
* skills: new L5r5eSetField({
|
||||
* options: [
|
||||
* { value: "athletics", label: "Athletics" },
|
||||
* { value: "meditation", label: "Meditation", disabled: true, tooltip: "Requires rank 3" },
|
||||
* ]
|
||||
* })
|
||||
*
|
||||
* // Renders automatically via {{formGroup}} in a Handlebars template:
|
||||
* // {{formGroup fields.skills name="skills" value=data.skills localize=true}}
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {{ value: string, label: string, disabled?: boolean, tooltip?: string }[]} options.options
|
||||
* Flat list of selectable items. Passed directly to {@link L5r5eHtmlMultiSelectElement.create}.
|
||||
* @param {object[]} [options.groups]
|
||||
* Optional optgroup definitions, forwarded to the element factory unchanged.
|
||||
* @param {boolean} [options.hideDisabledOptions=false]
|
||||
* When true, disabled options are hidden from the dropdown instead of greyed out.
|
||||
*/
|
||||
export class L5r5eSetField extends foundry.data.fields.SetField {
|
||||
|
||||
// We don't get the options we expect when we convert this to input,
|
||||
// So store them here
|
||||
/**
|
||||
* Saved constructor options, used to reconstruct the multiselect input on form render.
|
||||
* @type {object}
|
||||
*/
|
||||
#savedOptions;
|
||||
|
||||
constructor(options={}, context={}) {
|
||||
super(new foundry.data.fields.StringField({
|
||||
choices: options.options.map((option) => option.value)
|
||||
}), options, context);
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {object} context
|
||||
*/
|
||||
constructor(options = {}, context = {}) {
|
||||
super(
|
||||
new foundry.data.fields.StringField({
|
||||
choices: options.options?.map((option) => option.value) ?? [],
|
||||
}),
|
||||
options,
|
||||
context
|
||||
);
|
||||
|
||||
this.#savedOptions = options;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
initialize(value, model, options={}) {
|
||||
if ( !value ) return value;
|
||||
return new Set(super.initialize(value, model, options));
|
||||
/**
|
||||
* @param {*} value
|
||||
* @param {object} model
|
||||
* @param {object} options
|
||||
* @return {Set}
|
||||
* @override
|
||||
*/
|
||||
initialize(value, model, options = {}) {
|
||||
if (!value || (Array.isArray(value) && value.length === 0)) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
/** @override */
|
||||
return new Set(super.initialize(value, model, options).filter(Boolean));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Set} value
|
||||
* @return {*[]|*}
|
||||
* @override
|
||||
*/
|
||||
toObject(value) {
|
||||
if ( !value ) return value;
|
||||
return Array.from(value).map(v => this.element.toObject(v));
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
return Array.from(value).map((v) => this.element.toObject(v));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Form Field Integration */
|
||||
/* -------------------------------------------- */
|
||||
/**
|
||||
* @param {string|Array} value
|
||||
* @param {object} options
|
||||
* @return {Array}
|
||||
* @override
|
||||
*/
|
||||
clean(value, options) {
|
||||
// Settings forms submit comma-separated strings; split before normal cleaning.
|
||||
if (typeof value === "string") {
|
||||
value = value.length ? value.split(",").filter(Boolean) : [];
|
||||
}
|
||||
return super.clean(value, options);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
/**
|
||||
* @param {object} config
|
||||
* @return {L5r5eHtmlMultiSelectElement}
|
||||
* @override
|
||||
*/
|
||||
_toInput(config) {
|
||||
const e = this.element;
|
||||
return L5r5eHtmlMultiSelectElement.create({
|
||||
name: config.name,
|
||||
options: this.#savedOptions.options,
|
||||
groups: this.#savedOptions.groups,
|
||||
value: config.value,
|
||||
localize: config.localize
|
||||
localize: config.localize,
|
||||
hideDisabledOptions: this.#savedOptions.hideDisabledOptions,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -513,6 +513,9 @@ export class DicePickerDialog extends FormApplication {
|
||||
this._updateVoidPointUsage();
|
||||
this.render(false);
|
||||
});
|
||||
|
||||
// Open journal on effect name
|
||||
html.find(".effect-name").on("click", this._openEffectJournal.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -864,4 +867,34 @@ export class DicePickerDialog extends FormApplication {
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the core linked journal effect if exist
|
||||
* @param {Event} event
|
||||
* @private
|
||||
*/
|
||||
async _openEffectJournal(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const effectId = $(event.currentTarget).data("effect-id");
|
||||
if (!effectId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const effect = this._actor?.effects?.get(effectId);
|
||||
if (!effect?.system?.id && !effect?.system?.uuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const journal = await game.l5r5e.HelpersL5r5e.getObjectGameOrPack({
|
||||
id: effect.system.id,
|
||||
uuid: effect.system.uuid,
|
||||
type: "JournalEntry",
|
||||
});
|
||||
if (journal) {
|
||||
// Open on the "rules" section. If non exists then it will open the first page
|
||||
journal.sheet.render(true, {pageIndex: 2});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { HelpersL5r5e } from "../helpers.js";
|
||||
|
||||
/**
|
||||
* L5R Dice Roll n Keep dialog
|
||||
* @extends {FormApplication}
|
||||
@@ -268,6 +270,9 @@ export class RollnKeepDialog extends FormApplication {
|
||||
], { jQuery: false });
|
||||
}
|
||||
|
||||
// Open journal on effect name
|
||||
html.find(".effect-name").on("click", this._openEffectJournal.bind(this));
|
||||
|
||||
// *** Everything below here is only needed if the sheet is editable ***
|
||||
if (!this.isEditable) {
|
||||
return;
|
||||
@@ -648,7 +653,7 @@ export class RollnKeepDialog extends FormApplication {
|
||||
if (this.roll.l5r5e.isInitiativeRoll) {
|
||||
let msgOptions = {
|
||||
rnkRoll: this.roll,
|
||||
rollMode: game.l5r5e.HelpersL5r5e.getRollMode(this._message),
|
||||
messageMode: HelpersL5r5e.getMessageMode(this._message),
|
||||
};
|
||||
|
||||
await this.roll.l5r5e.actor.rollInitiative({
|
||||
@@ -664,7 +669,7 @@ export class RollnKeepDialog extends FormApplication {
|
||||
// Send it to chat, switch to new message
|
||||
this.message = await this.roll.toMessage(
|
||||
{},
|
||||
{ rollMode: game.l5r5e.HelpersL5r5e.getRollMode(this._message) }
|
||||
{ messageMode: HelpersL5r5e.getMessageMode(this._message) }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -802,4 +807,34 @@ export class RollnKeepDialog extends FormApplication {
|
||||
// Re-enable the button
|
||||
button.attr("disabled", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the core linked journal effect if exist
|
||||
* @param {Event} event
|
||||
* @private
|
||||
*/
|
||||
async _openEffectJournal(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const effectId = $(event.currentTarget).data("effect-id");
|
||||
if (!effectId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const effect = this.roll.l5r5e?.actor?.effects?.get(effectId);
|
||||
if (!effect?.system?.id && !effect?.system?.uuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const journal = await game.l5r5e.HelpersL5r5e.getObjectGameOrPack({
|
||||
id: effect.system.id,
|
||||
uuid: effect.system.uuid,
|
||||
type: "JournalEntry",
|
||||
});
|
||||
if (journal) {
|
||||
// Open on the "rules" section. If non exists then it will open the first page
|
||||
journal.sheet.render(true, {pageIndex: 2});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,18 +327,12 @@ export class RollL5r5e extends Roll {
|
||||
* This function can either create the ChatMessage directly, or return the data object that will be used to create.
|
||||
* @override
|
||||
*/
|
||||
async toMessage(messageData = {}, { rollMode = null } = {}) {
|
||||
async toMessage(messageData = {}, { messageMode = null } = {}) {
|
||||
// Perform the roll, if it has not yet been rolled
|
||||
if (!this._evaluated) {
|
||||
await this.evaluate();
|
||||
}
|
||||
|
||||
// RollMode
|
||||
const rMode = rollMode || messageData.rollMode || game.settings.get("core", "rollMode");
|
||||
if (rMode) {
|
||||
messageData = ChatMessage.applyRollMode(messageData, rMode);
|
||||
}
|
||||
|
||||
// Force the content to avoid weird foundry behaviour
|
||||
const content = this.l5r5e.dicesTypes.l5r ? await this.render({}) : this.total;
|
||||
|
||||
@@ -358,9 +352,15 @@ export class RollL5r5e extends Roll {
|
||||
);
|
||||
messageData.rolls = [this];
|
||||
|
||||
// Message mode
|
||||
const mMode = messageMode || messageData.messageMode || game.settings.get("core", "messageMode");
|
||||
if (mMode) {
|
||||
messageData = ChatMessage.applyMode(messageData, mMode);
|
||||
}
|
||||
|
||||
// Either create the message or just return the chat data
|
||||
return ChatMessage.implementation.create(messageData, {
|
||||
rollMode: rMode,
|
||||
messageMode: mMode,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -166,6 +166,15 @@ export class GmMonitor extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Apply global interface theme if it is set
|
||||
if (!this.options.classes.includes("themed")) {
|
||||
this.element.classList.remove("theme-light", "theme-dark");
|
||||
const {colorScheme} = game.settings.get("core", "uiConfig");
|
||||
if (colorScheme.interface) {
|
||||
this.element.classList.add("themed", `theme-${colorScheme.interface}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @override ApplicationV2 */
|
||||
|
||||
@@ -5,10 +5,11 @@ export class GmToolbox extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
/** @override ApplicationV2 */
|
||||
static get DEFAULT_OPTIONS() { return {
|
||||
id: "l5r5e-gm-toolbox",
|
||||
classes: ["faded-ui"],
|
||||
window: {
|
||||
contentClasses: ["l5r5e", "gm-toolbox", "faded-ui"],
|
||||
contentClasses: ["l5r5e", "gm-toolbox"],
|
||||
title: "l5r5e.gm.toolbox.title",
|
||||
minimizable: true,
|
||||
minimizable: false,
|
||||
},
|
||||
position: {
|
||||
width: "auto",
|
||||
@@ -89,6 +90,19 @@ export class GmToolbox extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
options.position.left = 220; //x - 630;
|
||||
}
|
||||
|
||||
async _onRender(context, options) {
|
||||
await super._onRender(context, options);
|
||||
|
||||
if (this.options.classes.includes("themed")) {
|
||||
return;
|
||||
}
|
||||
this.element.classList.remove("theme-light", "theme-dark");
|
||||
const {colorScheme} = game.settings.get("core", "uiConfig");
|
||||
if (colorScheme.interface) {
|
||||
this.element.classList.add("themed", `theme-${colorScheme.interface}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The GM Toolbox should not be removed when toggling the main menu with the esc key etc.
|
||||
* @override ApplicationV2
|
||||
|
||||
@@ -37,6 +37,14 @@ export const RegisterHandlebars = function () {
|
||||
return sanitizeIfFail(game.i18n.localize(isYes ? "Yes" : "No"));
|
||||
});
|
||||
|
||||
// Replace "localize (localize" for partial var (broken in v14)
|
||||
Handlebars.registerHelper("localizeEmbedded", function (string, objects) {
|
||||
Object.keys(objects.hash).forEach(key => {
|
||||
string = string.replace("{" + key + "}", objects.hash[key]);
|
||||
});
|
||||
return sanitizeIfFail(game.i18n.localize(string));
|
||||
});
|
||||
|
||||
/* ------------------------------------ */
|
||||
/* Dice */
|
||||
/* ------------------------------------ */
|
||||
|
||||
@@ -629,21 +629,21 @@ export class HelpersL5r5e {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the RollMode for this ChatData
|
||||
* Return the MessageMode for this ChatData
|
||||
* @param {object} chatData
|
||||
* @return {string}
|
||||
*/
|
||||
static getRollMode(chatData) {
|
||||
static getMessageMode(chatData) {
|
||||
if (chatData.whisper.length === 1 && chatData.whisper[0] === game.user.id) {
|
||||
return "selfroll";
|
||||
return "self";
|
||||
}
|
||||
if (chatData.blind) {
|
||||
return "blindroll";
|
||||
return "blind";
|
||||
}
|
||||
if (chatData.whisper.length > 1) {
|
||||
return "gmroll";
|
||||
return "gm";
|
||||
}
|
||||
return "roll";
|
||||
return "public";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -690,7 +690,7 @@ export class HelpersL5r5e {
|
||||
* @param {object} opt drawMany config option object
|
||||
* @return {Promise<{RollTableDraw}>} The drawn results
|
||||
*/
|
||||
static async drawManyFromPack(pack, tableName, retrieve = 5, opt = { rollMode: "selfroll" }) {
|
||||
static async drawManyFromPack(pack, tableName, retrieve = 5, opt = { messageMode: "self" }) {
|
||||
const comp = await game.packs.get(pack);
|
||||
if (!comp) {
|
||||
console.log(`L5R5E | Helpers | Pack not found[${pack}]`);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { L5r5eHtmlMultiSelectElement } from "./misc/l5r5e-multiselect.js";
|
||||
import { ItemCompendiumL5r5e } from "./compendium/l5r5e-item-compendium.js"
|
||||
|
||||
export default class HooksL5r5e {
|
||||
/**
|
||||
@@ -26,6 +26,8 @@ export default class HooksL5r5e {
|
||||
) {
|
||||
game.babele.setSystemTranslationsDir("babele"); // Since Babele v2.0.7
|
||||
}
|
||||
|
||||
ItemCompendiumL5r5e.applyToPacks();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,236 +247,6 @@ export default class HooksL5r5e {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compendium display (Add filters)
|
||||
*/
|
||||
static async renderCompendium(app, html, data) {
|
||||
html = $(html); // basic patch for v13
|
||||
|
||||
if (app.collection.documentName === "Item") {
|
||||
const content = await app.collection.getDocuments();
|
||||
const sourcesInThisCompendium = new Set([]);
|
||||
const filtersToShow = {
|
||||
rank: false,
|
||||
rarity: false,
|
||||
source: false,
|
||||
ring: false,
|
||||
};
|
||||
// Used to auto hide same values for a full compendium
|
||||
const previousValue = {
|
||||
rank: null,
|
||||
rarity: null,
|
||||
source: null,
|
||||
ring: null,
|
||||
};
|
||||
|
||||
// Cache
|
||||
const header = html.find(".directory-header");
|
||||
const entries = html.find(".directory-item");
|
||||
|
||||
// Add additional data to the entries to make it faster to lookup.
|
||||
// Add Ring/rank/rarity information
|
||||
for (const document of content) {
|
||||
const entry = entries.filter(`[data-entry-id="${document.id}"]`);
|
||||
|
||||
// Hide filter if only one value of this type is found in the compendium
|
||||
const autoDisplayFilter = (props, documentData = null) => {
|
||||
documentData ??= document.system[props];
|
||||
|
||||
if (filtersToShow[props] || previousValue[props] === documentData) {
|
||||
return;
|
||||
}
|
||||
filtersToShow[props] = previousValue[props] !== null && previousValue[props] !== documentData;
|
||||
previousValue[props] = documentData;
|
||||
};
|
||||
|
||||
if (document.system?.rank) {
|
||||
autoDisplayFilter('rank');
|
||||
entry.data("rank", document.system.rank);
|
||||
}
|
||||
|
||||
if (document.system?.source_reference.source) {
|
||||
autoDisplayFilter('source', document.system.source_reference.source);
|
||||
sourcesInThisCompendium.add(document.system.source_reference.source);
|
||||
entry.data("source", document.system.source_reference);
|
||||
}
|
||||
|
||||
if (document.system?.ring) {
|
||||
autoDisplayFilter('ring');
|
||||
entry.data("ring", document.system.ring);
|
||||
}
|
||||
|
||||
if (document.system?.rarity) {
|
||||
autoDisplayFilter('rarity');
|
||||
entry.data("rarity", document.system.rarity);
|
||||
}
|
||||
|
||||
// Add ring/rank/rarity information on the item in the compendium view
|
||||
if (document.system?.ring || document.system?.rarity || document.system?.rank) {
|
||||
const ringRarityRank = await foundry.applications.handlebars.renderTemplate(`${CONFIG.l5r5e.paths.templates}compendium/ring-rarity-rank.html`, document.system);
|
||||
entry.append(ringRarityRank);
|
||||
}
|
||||
}
|
||||
|
||||
// Setup filters
|
||||
const officialContentSet = game.settings.get(CONFIG.l5r5e.namespace, "compendium-official-content-for-players");
|
||||
const unofficialContentSet = game.settings.get(CONFIG.l5r5e.namespace, "compendium-unofficial-content-for-players");
|
||||
const allCompendiumReferencesSet = game.settings.get(CONFIG.l5r5e.namespace, "all-compendium-references")
|
||||
const hideEmptySourcesFromPlayers = game.settings.get(CONFIG.l5r5e.namespace, "compendium-hide-empty-sources-from-players");
|
||||
|
||||
const unavailableSourceForPlayersSet = new Set([...allCompendiumReferencesSet].filter((element) => {
|
||||
if (CONFIG.l5r5e.sourceReference[element]) {
|
||||
return officialContentSet.size > 0 ? !officialContentSet.has(element) : false;
|
||||
}
|
||||
return unofficialContentSet.size > 0 ? !unofficialContentSet.has(element) : false;
|
||||
}));
|
||||
|
||||
// Create filter function
|
||||
const applyCompendiumFilter = () => {
|
||||
const userFilter = header.find("l5r5e-multi-select").val();
|
||||
const rankFilter = header.find(".rank-filter .selected").data("rank");
|
||||
const ringFilter = header.find(".ring-filter .selected").data("ring");
|
||||
const rarityFilter = header.find(".rarity-filter .selected").data("rarity");
|
||||
|
||||
entries.each(function () {
|
||||
const lineSource = $(this).data("source")?.source;
|
||||
|
||||
// We might have stuff in the compendium view that does not have a source (folders etc.) Ignore those.
|
||||
if (lineSource === null || lineSource === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let shouldShow = true;
|
||||
|
||||
// Handle unavailable sources
|
||||
if (unavailableSourceForPlayersSet.has(lineSource)) {
|
||||
if (game.user.isGM) {
|
||||
shouldShow &= true;
|
||||
$(this)
|
||||
.addClass("not-for-players")
|
||||
.attr("data-tooltip", game.i18n.localize("l5r5e.compendium.not_for_players"));
|
||||
} else {
|
||||
shouldShow &= false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle empty sources
|
||||
if (lineSource === "" && hideEmptySourcesFromPlayers) {
|
||||
if (game.user.isGM) {
|
||||
shouldShow &= true;
|
||||
$(this)
|
||||
.addClass("not-for-players")
|
||||
.attr("data-tooltip", game.i18n.localize("l5r5e.compendium.not_for_players"));
|
||||
} else {
|
||||
shouldShow &= false;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
if (rankFilter) {
|
||||
shouldShow &= $(this).data("rank") == rankFilter;
|
||||
}
|
||||
if (userFilter?.length) {
|
||||
shouldShow &= userFilter.includes(lineSource);
|
||||
}
|
||||
if (ringFilter) {
|
||||
shouldShow &= $(this).data("ring") == ringFilter;
|
||||
}
|
||||
if (rarityFilter >= 0) {
|
||||
shouldShow &= $(this).data("rarity") == rarityFilter;
|
||||
}
|
||||
|
||||
// Show or hide this entry based on the result
|
||||
shouldShow ? $(this).show() : $(this).hide();
|
||||
});
|
||||
};
|
||||
|
||||
// Filter setup
|
||||
const addFilter = async (filterType, templateFile, templateData) => {
|
||||
if (!filtersToShow[filterType]) {
|
||||
return;
|
||||
}
|
||||
const filterTemplate = await foundry.applications.handlebars.renderTemplate(
|
||||
`${CONFIG.l5r5e.paths.templates}compendium/${templateFile}.html`,
|
||||
templateData
|
||||
);
|
||||
header.append(filterTemplate);
|
||||
|
||||
header.find(`.${filterType}-filter`).children().each(function () {
|
||||
$(this).on("click", (event) => {
|
||||
const selected = $(event.target).hasClass("selected");
|
||||
header.find(`.${filterType}-filter`).children().removeClass("selected");
|
||||
$(event.target).toggleClass("selected", !selected);
|
||||
applyCompendiumFilter();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Add Rank, Rarity, Ring Filters
|
||||
await Promise.all([
|
||||
addFilter('rank' , 'rank-filter', { type: "rank", number: [1, 2, 3, 4, 5] }),
|
||||
addFilter('rarity', 'rank-filter', { type: "rarity", number: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }),
|
||||
addFilter('ring' , 'ring-filter', {}),
|
||||
]);
|
||||
|
||||
if (filtersToShow.source) {
|
||||
// Build the source select
|
||||
const selectableSourcesArray = [...allCompendiumReferencesSet].map((reference) => ({
|
||||
value: reference,
|
||||
label: CONFIG.l5r5e.sourceReference[reference]?.label ?? reference,
|
||||
translate: true,
|
||||
group: CONFIG.l5r5e.sourceReference[reference]?.type.split(",")[0] ?? "l5r5e.multiselect.sources_categories.others",
|
||||
disabled: !sourcesInThisCompendium.has(reference) || (!game.user.isGM && unavailableSourceForPlayersSet.has(reference))
|
||||
}));
|
||||
const filterSourcesBox = L5r5eHtmlMultiSelectElement.create({
|
||||
name: "filter-sources",
|
||||
options: selectableSourcesArray,
|
||||
localize: true,
|
||||
});
|
||||
header.append(filterSourcesBox.outerHTML);
|
||||
$("l5r5e-multi-select").on("change", applyCompendiumFilter);
|
||||
|
||||
// If gm add an extra button to easily filter the content to see the same stuff as a player
|
||||
if (game.user.isGM && unavailableSourceForPlayersSet.size > 0) {
|
||||
const buttonHTML = `<button type="button" class="gm" data-tooltip="${game.i18n.localize('l5r5e.multiselect.player_filter_tooltip')}">`
|
||||
+ game.i18n.localize('l5r5e.multiselect.player_filter_label')
|
||||
+ '</button>'
|
||||
|
||||
const filterPlayerViewArray = [...allCompendiumReferencesSet]
|
||||
.filter((item) => !unavailableSourceForPlayersSet.has(item))
|
||||
.filter((item) => sourcesInThisCompendium.has(item));
|
||||
|
||||
$(buttonHTML).appendTo($(header).find("l5r5e-multi-select")).click(function() {
|
||||
header.find("l5r5e-multi-select")[0].value = filterPlayerViewArray;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This delay is a workaround and should be addressed in another way.
|
||||
// This is ugly but if we hide the content too early then it won't be hidden for some reason.
|
||||
// Current guess is that the foundry search filter is doing something.
|
||||
// Adding a delay here so that we hide the content. This will fail on slow computers/network...
|
||||
setTimeout(() => {
|
||||
applyCompendiumFilter();
|
||||
}, 250);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static updateCompendium(pack, documents, options, userId) {
|
||||
documents.forEach((document) => {
|
||||
const inc_reference = document?.system?.source_reference?.source?.trim();
|
||||
if (!!inc_reference) {
|
||||
const references = game.settings.get(CONFIG.l5r5e.namespace, "all-compendium-references");
|
||||
if (!references.includes(inc_reference)) {
|
||||
references.push(inc_reference);
|
||||
game.settings.set(CONFIG.l5r5e.namespace, "all-compendium-references", references);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* DiceSoNice - Add L5R DicePresets
|
||||
*/
|
||||
|
||||
@@ -149,5 +149,8 @@ export class AdvancementSheetL5r5e extends ItemSheetL5r5e {
|
||||
xp_used: xp_used,
|
||||
},
|
||||
});
|
||||
|
||||
// Re-render sheet
|
||||
this.render(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ActorL5r5e } from "./actor.js";
|
||||
import { CharacterSheetL5r5e } from "./actors/character-sheet.js";
|
||||
import { NpcSheetL5r5e } from "./actors/npc-sheet.js";
|
||||
import { ArmySheetL5r5e } from "./actors/army-sheet.js";
|
||||
import { RulerL5r5e, TokenRulerL5r5e } from "./tatical-grid-rulers.js";
|
||||
// Dice and rolls
|
||||
import { L5rBaseDie } from "./dice/dietype/l5r-base-die.js";
|
||||
import { AbilityDie } from "./dice/dietype/ability-die.js";
|
||||
@@ -37,6 +38,8 @@ import { ArmyFortificationSheetL5r5e } from "./items/army-fortification-sheet.js
|
||||
// JournalEntry
|
||||
import { JournalL5r5e } from "./journal.js";
|
||||
import { BaseJournalSheetL5r5e } from "./journals/base-journal-sheet.js";
|
||||
// Compendium
|
||||
import { CompendiumDirectoryL5r5e } from "./compendium/l5r5e-compendium-directory.js";
|
||||
// Specific
|
||||
import { MigrationL5r5e } from "./migration.js";
|
||||
import { GmToolbox } from "./gm/gm-toolbox.js";
|
||||
@@ -44,8 +47,10 @@ import { GmMonitor } from "./gm/gm-monitor.js";
|
||||
import { Storage } from "./storage.js";
|
||||
// Misc
|
||||
import { L5r5eHtmlMultiSelectElement } from "./misc/l5r5e-multiselect.js";
|
||||
import { L5R5eHtmlComboBoxElement } from "./misc/l5r5e-combo-box.js";
|
||||
|
||||
window.customElements.define(L5r5eHtmlMultiSelectElement.tagName, L5r5eHtmlMultiSelectElement);
|
||||
window.customElements.define(L5R5eHtmlComboBoxElement.tagName, L5R5eHtmlComboBoxElement);
|
||||
|
||||
/* ------------------------------------ */
|
||||
/* Initialize system */
|
||||
@@ -65,6 +70,23 @@ Hooks.once("init", async () => {
|
||||
// Global access to L5R Config
|
||||
CONFIG.l5r5e = L5R5E;
|
||||
|
||||
// Setting up sidebar icons
|
||||
CONFIG.ChatMessage.sidebarIcon = "l5r5e chatIcon";
|
||||
CONFIG.Combat.sidebarIcon = "l5r5e combatIcon";
|
||||
CONFIG.Scene.sidebarIcon = "l5r5e sceneIcon";
|
||||
CONFIG.Actor.sidebarIcon = "l5r5e actorIcon";
|
||||
CONFIG.Item.sidebarIcon = "l5r5e itemIcon";
|
||||
CONFIG.JournalEntry.sidebarIcon = "l5r5e journalIcon";
|
||||
CONFIG.RollTable.sidebarIcon = "l5r5e rolltableIcon";
|
||||
CONFIG.Playlist.sidebarIcon = "l5r5e playlistIcon";
|
||||
// Note: We don't have any custom icons here so just append l5r5e and type
|
||||
CONFIG.Cards.sidebarIcon += " l5r5e cardsIcon";
|
||||
CONFIG.Macro.sidebarIcon += " l5r5e macroIcon";
|
||||
|
||||
// The compendium and the settings menu is registered a little different.
|
||||
foundry.applications.sidebar.Sidebar.TABS.compendium.icon = "l5r5e compendiumIcon";
|
||||
foundry.applications.sidebar.Sidebar.TABS.settings.icon = "l5r5e settingsIcon";
|
||||
|
||||
// Assign custom classes and constants here
|
||||
CONFIG.Combat.documentClass = CombatL5r5e;
|
||||
CONFIG.Actor.documentClass = ActorL5r5e;
|
||||
@@ -72,6 +94,10 @@ Hooks.once("init", async () => {
|
||||
CONFIG.Item.documentClass = ItemL5r5e;
|
||||
CONFIG.JournalEntry.documentClass = JournalL5r5e;
|
||||
CONFIG.JournalEntry.sheetClass = BaseJournalSheetL5r5e;
|
||||
CONFIG.Token.rulerClass = TokenRulerL5r5e;
|
||||
CONFIG.Canvas.rulerClass = RulerL5r5e;
|
||||
|
||||
CONFIG.ui.compendium = CompendiumDirectoryL5r5e;
|
||||
|
||||
// Define custom Roll class
|
||||
CONFIG.Dice.rolls.unshift(RollL5r5e);
|
||||
@@ -262,6 +288,4 @@ Hooks.on("renderSidebarTab", (app, html, data) => HooksL5r5e.renderSidebarTab(ap
|
||||
Hooks.on("activateSettings", async (app)=> HooksL5r5e.activateSettings(app));
|
||||
Hooks.on("renderChatMessageHTML", (message, html, data) => HooksL5r5e.renderChatMessage(message, html, data));
|
||||
Hooks.on("renderCombatTracker", (app, html, data) => HooksL5r5e.renderCombatTracker(app, html, data));
|
||||
Hooks.on("renderCompendium", async (app, html, data) => HooksL5r5e.renderCompendium(app, html, data));
|
||||
Hooks.on("diceSoNiceRollStart", (messageId, context) => HooksL5r5e.diceSoNiceRollStart(messageId, context));
|
||||
Hooks.on("updateCompendium", (pack, documents, options, userId) => HooksL5r5e.updateCompendium(pack, documents, options, userId));
|
||||
|
||||
257
system/scripts/misc/l5r5e-combo-box.js
Normal file
257
system/scripts/misc/l5r5e-combo-box.js
Normal file
@@ -0,0 +1,257 @@
|
||||
import { DropdownMixin } from "./l5r5e-dropdown-mixin.js";
|
||||
|
||||
const { AbstractFormInputElement } = foundry.applications.elements;
|
||||
|
||||
/**
|
||||
* A custom `<l5r5e-combo-box>` combining a free-text input with a filterable option dropdown.
|
||||
*
|
||||
* Stores a **single string value** — either a predefined option's `value` attribute, or
|
||||
* whatever the user typed freely. Use this when a field holds exactly one value, whether
|
||||
* chosen from a list or entered manually (e.g. a weapon name, a title, a custom skill).
|
||||
* For storing multiple values from a list, use {@link L5r5eHtmlMultiSelectElement} instead.
|
||||
*
|
||||
* Picking a predefined option stores its `value` attribute while displaying its human-readable
|
||||
* label. Free-typing stores the typed string as both value and label. Fires `input` on every
|
||||
* keystroke and `change` only on commit (blur or Enter) to avoid triggering sheet re-renders
|
||||
* mid-typing. Picking from the dropdown fires both `input` and `change` immediately.
|
||||
*
|
||||
* Use `{{selectOptions}}` without `selected=` to render the available options, and set the
|
||||
* current value via the `value` attribute on the element directly.
|
||||
*
|
||||
* @example
|
||||
* ```hbs
|
||||
* {{!-- Pass current value via the element's `value` attribute, not selectOptions selected= --}}
|
||||
* <l5r5e-combo-box name="weapon" value="{{data.weapon}}">
|
||||
* {{selectOptions choices localize=true}}
|
||||
* </l5r5e-combo-box>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* // Programmatic update — query the element by its name attribute, then set value directly.
|
||||
* // The visible input label updates automatically to match the selected option's label.
|
||||
* const el = document.querySelector("l5r5e-combo-box[name='weapon']");
|
||||
* el.value = "axe"; // input shows "Battleaxe", el.value returns "axe"
|
||||
*
|
||||
* // Free-text entry (no matching option) — value and label are both set to the typed string:
|
||||
* el.value = "naginata"; // input shows "naginata", el.value returns "naginata"
|
||||
*/
|
||||
export class L5R5eHtmlComboBoxElement extends DropdownMixin(
|
||||
AbstractFormInputElement,
|
||||
{ multiSelect: false, debounceMs: 150, clearOnClose: false }
|
||||
) {
|
||||
/**
|
||||
* The label currently shown in the text input. Differs from `_value` when a predefined
|
||||
* option is selected (value = option's value attribute, label = option's display text).
|
||||
* @type {string}
|
||||
*/
|
||||
_label = "";
|
||||
|
||||
/** @override */
|
||||
static tagName = "l5r5e-combo-box";
|
||||
|
||||
/** @override */
|
||||
static observedAttributes = ["disabled", "placeholder", "value"];
|
||||
|
||||
/**
|
||||
* Flat descriptor list built once in _initialize(), mirrors the light-DOM options.
|
||||
* @type {{ value: string, label: string, group: string|null }[]}
|
||||
*/
|
||||
#options = [];
|
||||
|
||||
/**
|
||||
* Value snapshot taken when the input receives focus.
|
||||
* Used by the blur handler to decide whether to fire a change event.
|
||||
* @type {string}
|
||||
*/
|
||||
#valueAtFocus = "";
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Accessors */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
get value() {
|
||||
return this._value ?? "";
|
||||
}
|
||||
|
||||
set value(value) {
|
||||
const match = this.#options.find(option => option.value === String(value ?? ""));
|
||||
if (match) {
|
||||
this._value = match.value;
|
||||
this._label = match.label;
|
||||
}
|
||||
else {
|
||||
this._value = String(value ?? "");
|
||||
this._label = this._value;
|
||||
}
|
||||
this._internals.setFormValue(this._value);
|
||||
if (this._dropdownInput) {
|
||||
this._dropdownInput.value = this._label;
|
||||
}
|
||||
this.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
|
||||
this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
* AbstractFormInputElement does not have an _initialize() hook, so we override
|
||||
* connectedCallback to snapshot options from the light DOM before _buildElements()
|
||||
* is called by super.connectedCallback().
|
||||
*
|
||||
* We keep this minimal: just build the flat #options list so _getDropdownOptions()
|
||||
* and the value setter can look options up by value.
|
||||
* @override
|
||||
*/
|
||||
connectedCallback() {
|
||||
this.#snapshotOptions();
|
||||
this.#resolveInitialValue();
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
#snapshotOptions() {
|
||||
const makeOption = (option, group = null) => ({
|
||||
value: option.value,
|
||||
label: option.innerText,
|
||||
group,
|
||||
});
|
||||
|
||||
this.#options = [...this.children].flatMap(child => {
|
||||
if (child instanceof HTMLOptGroupElement) {
|
||||
return [...child.querySelectorAll("option")]
|
||||
.filter(option => option.value)
|
||||
.map(option => makeOption(option, child.label));
|
||||
}
|
||||
if (child instanceof HTMLOptionElement && child.value) {
|
||||
return makeOption(child);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
#resolveInitialValue() {
|
||||
// Honour a `value` attribute if set, otherwise find a selected option.
|
||||
const attrValue = this.getAttribute("value");
|
||||
const initial = attrValue ?? this.#options.find(option => option.selected)?.value ?? "";
|
||||
const match = this.#options.find(option => option.value === initial);
|
||||
if (match) {
|
||||
this._value = match.value;
|
||||
this._label = match.label;
|
||||
} else {
|
||||
this._value = initial;
|
||||
this._label = initial;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Element Lifecycle */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_buildElements() {
|
||||
const wrapper = this._buildDropdownElements({
|
||||
placeholder: this.getAttribute("placeholder") ?? "",
|
||||
});
|
||||
|
||||
this._dropdownInput.value = this._label || this._value || "";
|
||||
this._primaryInput = this._dropdownInput;
|
||||
|
||||
return [wrapper];
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_activateListeners() {
|
||||
const signal = this.abortSignal;
|
||||
this._activateDropdownListeners();
|
||||
|
||||
// Prevent the inner <input>'s own native change from reaching Foundry's form handler.
|
||||
// We dispatch our own change at commit time only (see below).
|
||||
this._dropdownInput.addEventListener("change", (e) => e.stopPropagation(), { signal });
|
||||
|
||||
// Free typing: update value and fire `input` immediately.
|
||||
// Do NOT fire `change` here — Foundry's form handler re-renders the sheet on
|
||||
// every `change` event, which would destroy the element mid-typing.
|
||||
// `change` is fired at commit time: on blur (if value changed) or Enter.
|
||||
this._dropdownInput.addEventListener("input", () => {
|
||||
const typed = this._dropdownInput.value;
|
||||
this._label = typed;
|
||||
this._value = typed;
|
||||
this._internals.setFormValue(typed);
|
||||
this.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
|
||||
// DropdownMixin's #onInput fires the debounced dropdown re-render.
|
||||
}, { signal });
|
||||
|
||||
// Commit on blur if the value has changed since the element was focused.
|
||||
this._dropdownInput.addEventListener("focus", () => {
|
||||
this.#valueAtFocus = this._value;
|
||||
}, { signal });
|
||||
|
||||
this._dropdownInput.addEventListener("blur", () => {
|
||||
if (this._value !== this.#valueAtFocus) {
|
||||
this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
|
||||
}
|
||||
}, { signal });
|
||||
|
||||
// Commit on Enter for free-text values (not picked from the dropdown).
|
||||
this._dropdownInput.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
this._dropdownInput.blur();
|
||||
}
|
||||
}, { signal });
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_toggleDisabled(disabled) {
|
||||
this._toggleDropdownDisabled(disabled);
|
||||
// Add/remove .disabled on the wrapper so CSS can dim the whole control.
|
||||
const wrapper = this.querySelector(".wrapper");
|
||||
if (wrapper) {
|
||||
wrapper.classList.toggle("disabled", disabled);
|
||||
}
|
||||
}
|
||||
|
||||
/** @override */
|
||||
attributeChangedCallback(attrName, oldValue, newValue) {
|
||||
super.attributeChangedCallback(attrName, oldValue, newValue);
|
||||
if (attrName === "disabled") {
|
||||
this._toggleDropdownDisabled(newValue !== null);
|
||||
}
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_refresh() {
|
||||
if (!this._dropdownInput)
|
||||
return;
|
||||
|
||||
this._dropdownInput.value = this._label ?? "";
|
||||
this._internals.setFormValue(this._value ?? "");
|
||||
this._dropdownRefresh();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* DropdownMixin Contract */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_getDropdownOptions() {
|
||||
return this.#options;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_isOptionSelected(value) {
|
||||
return this._value === value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit the picked option as the current value and close the dropdown.
|
||||
* @override
|
||||
*/
|
||||
_onDropdownPick(option) {
|
||||
this._value = option.value;
|
||||
this._label = option.label;
|
||||
this._dropdownInput.value = option.label;
|
||||
this._internals.setFormValue(option.value);
|
||||
this._dropdownInput.blur();
|
||||
this.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
|
||||
this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
|
||||
}
|
||||
}
|
||||
492
system/scripts/misc/l5r5e-dropdown-mixin.js
Normal file
492
system/scripts/misc/l5r5e-dropdown-mixin.js
Normal file
@@ -0,0 +1,492 @@
|
||||
/**
|
||||
* @typedef {Object} DropdownMixinConfig
|
||||
* @property {boolean} [multiSelect=false]
|
||||
* When true the dropdown toggles selection and stays open after a pick.
|
||||
* When false a pick commits the value and closes (combo-box mode).
|
||||
* @property {number} [debounceMs=150]
|
||||
* Trailing-debounce delay in milliseconds for dropdown re-renders while typing.
|
||||
*/
|
||||
|
||||
/**
|
||||
* DropdownMixin
|
||||
*
|
||||
* Adds a fully custom `<ul role="listbox">` dropdown to any AbstractFormInputElement
|
||||
* subclass. Handles UI only — building, opening, closing, rendering options, and keyboard
|
||||
* navigation. Has no opinion about how options are stored or how values are managed.
|
||||
*
|
||||
* ### Contract: three methods the host must implement
|
||||
*
|
||||
* - `_getDropdownOptions()`
|
||||
* Return `{ value, label, group, disabled, tooltip }[]` — the full unfiltered list.
|
||||
*
|
||||
* - `_isOptionSelected(value)` → boolean
|
||||
* Return whether a given value is currently selected.
|
||||
*
|
||||
* - `_onDropdownPick(option)` → void
|
||||
* Called when the user picks an option. The mixin handles post-pick behaviour
|
||||
* (close in single mode, re-render in multi mode) after this returns.
|
||||
*
|
||||
* ### Host responsibilities
|
||||
* - Call `this._buildDropdownElements({ placeholder })` inside `_buildElements()`
|
||||
* and include the returned wrapper in the returned array.
|
||||
* - Call `this._activateDropdownListeners()` inside `_activateListeners()`.
|
||||
* - Call `this._toggleDropdownDisabled(disabled)` inside `_toggleDisabled()`.
|
||||
* - Call `this._dropdownRefresh()` inside `_refresh()` to keep checkmarks in sync.
|
||||
*
|
||||
* @param {typeof AbstractFormInputElement} Base
|
||||
* @param {DropdownMixinConfig} [mixinConfig={}]
|
||||
* @return {typeof Base}
|
||||
*/
|
||||
export function DropdownMixin(Base, mixinConfig = {}) {
|
||||
const {
|
||||
multiSelect = false,
|
||||
debounceMs = 150,
|
||||
/**
|
||||
* When true, closing the dropdown clears the search input text.
|
||||
* Use true for multi-select (search is transient) and false for
|
||||
* combo-box (the input IS the value and must persist after close).
|
||||
*/
|
||||
clearOnClose = true,
|
||||
} = mixinConfig;
|
||||
|
||||
return class DropdownMixinElement extends Base {
|
||||
|
||||
/* --------------------------------------------------------- */
|
||||
/* Private Fields */
|
||||
/* --------------------------------------------------------- */
|
||||
|
||||
/** @type {HTMLInputElement} */
|
||||
#searchInput;
|
||||
|
||||
/** @type {HTMLUListElement} */
|
||||
#list;
|
||||
|
||||
/** @type {boolean} */
|
||||
#open = false;
|
||||
|
||||
/**
|
||||
* Snapshot of #open at the moment a focus event fires.
|
||||
* Lets us distinguish a fresh focus (should open) from a click
|
||||
* on an already-focused input (should toggle closed).
|
||||
* @type {boolean}
|
||||
*/
|
||||
#wasOpenOnFocus = false;
|
||||
|
||||
/** @type {number} */
|
||||
#activeIndex = -1;
|
||||
|
||||
/** @type {HTMLLIElement[]} — flat list of pickable <li>s, excludes group headers */
|
||||
#optionElements = [];
|
||||
|
||||
/** @type {Function} */
|
||||
#debouncedOpen;
|
||||
|
||||
/**
|
||||
* Per-instance key so multiple elements on the same page never share a timer.
|
||||
* @type {string}
|
||||
*/
|
||||
#debounceId = `l5r5e-dropdown-${foundry.utils.randomID()}`;
|
||||
|
||||
/**
|
||||
* The search input element. The host should assign this to `this._primaryInput`.
|
||||
* @return {HTMLInputElement}
|
||||
*/
|
||||
get _dropdownInput() {
|
||||
return this.#searchInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the search `<input>` + `<ul>` dropdown inside a positioned wrapper div.
|
||||
* Include the returned element in the array returned from `_buildElements()`.
|
||||
*
|
||||
* @param {object} [opts]
|
||||
* @param {string} [opts.placeholder=""]
|
||||
* @return {HTMLDivElement}
|
||||
*/
|
||||
_buildDropdownElements({ placeholder = "" } = {}) {
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.classList.add("wrapper");
|
||||
|
||||
this.#searchInput = document.createElement("input");
|
||||
this.#searchInput.type = "text";
|
||||
this.#searchInput.classList.add("input");
|
||||
this.#searchInput.setAttribute("autocomplete", "off");
|
||||
this.#searchInput.setAttribute("role", "combobox");
|
||||
this.#searchInput.setAttribute("aria-autocomplete", "list");
|
||||
this.#searchInput.setAttribute("aria-expanded", "false");
|
||||
this.#searchInput.setAttribute("aria-haspopup", "listbox");
|
||||
if (placeholder) {
|
||||
this.#searchInput.setAttribute("placeholder", placeholder);
|
||||
}
|
||||
|
||||
this.#list = document.createElement("ul");
|
||||
this.#list.classList.add("dropdown");
|
||||
this.#list.setAttribute("role", "listbox");
|
||||
if (multiSelect) {
|
||||
this.#list.setAttribute("aria-multiselectable", "true");
|
||||
}
|
||||
this.#list.hidden = true;
|
||||
|
||||
this.#syncOffset();
|
||||
wrapper.append(this.#searchInput, this.#list);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
/** Attach dropdown event listeners. Call inside `_activateListeners()`. */
|
||||
_activateDropdownListeners() {
|
||||
const signal = this.abortSignal;
|
||||
|
||||
this.#debouncedOpen = game.l5r5e.HelpersL5r5e.debounce(
|
||||
this.#debounceId,
|
||||
(query) => this.#openDropdown(query),
|
||||
debounceMs,
|
||||
false // trailing — fires after the user pauses
|
||||
);
|
||||
|
||||
this.#searchInput.addEventListener("mousedown", this.#onMouseDown.bind(this), { signal });
|
||||
this.#searchInput.addEventListener("focus", this.#onFocus.bind(this), { signal });
|
||||
this.#searchInput.addEventListener("input", this.#onInput.bind(this), { signal });
|
||||
this.#searchInput.addEventListener("keydown", this.#onKeydown.bind(this), { signal });
|
||||
this.#searchInput.addEventListener("blur", this.#onBlur.bind(this), { signal });
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable the search input. Call inside `_toggleDisabled()`.
|
||||
* @param {boolean} disabled
|
||||
*/
|
||||
_toggleDropdownDisabled(disabled) {
|
||||
if (this.#searchInput) {
|
||||
this.#searchInput.disabled = disabled;
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------- */
|
||||
/* Refresh */
|
||||
/* --------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Re-render the open dropdown in place so checkmarks and disabled states
|
||||
* stay in sync with the current value. Call inside `_refresh()`.
|
||||
*/
|
||||
_dropdownRefresh() {
|
||||
if (this.#open) {
|
||||
this.#renderOptions(this.#filter(this.#searchInput?.value ?? ""));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @return {{ value: string, label: string, group: string|null, disabled?: boolean, tooltip?: string }[]}
|
||||
*/
|
||||
_getDropdownOptions() {
|
||||
throw new Error(`${this.constructor.name} must implement _getDropdownOptions()`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @param {string} value
|
||||
* @return {boolean}
|
||||
*/
|
||||
_isOptionSelected(value) {
|
||||
throw new Error(`${this.constructor.name} must implement _isOptionSelected()`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @param {{ value: string, label: string }} option
|
||||
*/
|
||||
_onDropdownPick(option) {
|
||||
throw new Error(`${this.constructor.name} must implement _onDropdownPick()`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Case-insensitive label filter over `_getDropdownOptions()`.
|
||||
* Returns all options when query is empty.
|
||||
* @param {string} query
|
||||
* @return {object[]}
|
||||
*/
|
||||
#filter(query) {
|
||||
const all = this._getDropdownOptions();
|
||||
if (!query) {
|
||||
return all;
|
||||
}
|
||||
const lower = query.toLowerCase();
|
||||
return all.filter(option => option.label.toLowerCase().includes(lower));
|
||||
}
|
||||
|
||||
#openDropdown(query = "") {
|
||||
this.#renderOptions(this.#filter(query));
|
||||
this.#list.hidden = false;
|
||||
this.#searchInput.setAttribute("aria-expanded", "true");
|
||||
this.#open = true;
|
||||
this.#activeIndex = -1;
|
||||
this._onDropdownOpened();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the dropdown opens. Override in host classes to snapshot
|
||||
* the current value so _onDropdownClosed can compare against it.
|
||||
* Default is a no-op.
|
||||
* @protected
|
||||
*/
|
||||
_onDropdownOpened() {}
|
||||
|
||||
#closeDropdown() {
|
||||
this.#list.hidden = true;
|
||||
this.#searchInput.setAttribute("aria-expanded", "false");
|
||||
this.#open = false;
|
||||
this.#wasOpenOnFocus = false;
|
||||
this.#activeIndex = -1;
|
||||
this.#optionElements = [];
|
||||
// In combo-box mode the input IS the value, so we leave it intact.
|
||||
// In multi-select mode the search text is transient and should reset.
|
||||
if (clearOnClose && this.#searchInput) {
|
||||
this.#searchInput.value = "";
|
||||
}
|
||||
// Notify the host that the dropdown has closed. Hosts can override this
|
||||
// to fire a single change event after a multi-pick session.
|
||||
this._onDropdownClosed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the dropdown closes. Override in host classes to fire a
|
||||
* consolidated change event after a multi-pick session.
|
||||
* Default is a no-op.
|
||||
* @protected
|
||||
*/
|
||||
_onDropdownClosed() {}
|
||||
|
||||
#renderOptions(options) {
|
||||
this.#list.innerHTML = "";
|
||||
this.#optionElements = [];
|
||||
|
||||
// hideDisabledOptions is a host-level attribute, read via the DOM.
|
||||
const visible = this.hasAttribute("hidedisabledoptions")
|
||||
? options.filter(o => !o.disabled)
|
||||
: options;
|
||||
|
||||
if (!visible.length) {
|
||||
const empty = document.createElement("li");
|
||||
empty.classList.add("no-results");
|
||||
empty.textContent = game.i18n.localize("l5r5e.multiselect.no_results") ?? "No matches";
|
||||
empty.setAttribute("aria-disabled", "true");
|
||||
this.#list.append(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
// Bucket into groups, preserving insertion order.
|
||||
const groups = new Map();
|
||||
const ungrouped = [];
|
||||
for (const option of visible) {
|
||||
if (option.group) {
|
||||
if (!groups.has(option.group)) groups.set(option.group, []);
|
||||
groups.get(option.group).push(option);
|
||||
} else {
|
||||
ungrouped.push(option);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [label, options] of groups) {
|
||||
const header = document.createElement("li");
|
||||
header.classList.add("group");
|
||||
header.setAttribute("role", "presentation");
|
||||
header.textContent = label;
|
||||
this.#list.append(header);
|
||||
for (const option of options) {
|
||||
this.#list.append(this.#buildOptionEl(option));
|
||||
}
|
||||
}
|
||||
for (const option of ungrouped){
|
||||
this.#list.append(this.#buildOptionEl(option));
|
||||
}
|
||||
}
|
||||
|
||||
#buildOptionEl(option) {
|
||||
const selected = this._isOptionSelected(option.value);
|
||||
const disabled = !!option.disabled;
|
||||
|
||||
const li = document.createElement("li");
|
||||
li.classList.add("option");
|
||||
if (selected) {
|
||||
li.classList.add("selected");
|
||||
}
|
||||
if (disabled) {
|
||||
li.classList.add("disabled");
|
||||
}
|
||||
li.setAttribute("role", "option");
|
||||
li.setAttribute("aria-selected", String(selected));
|
||||
li.setAttribute("aria-disabled", String(disabled));
|
||||
li.dataset.value = option.value;
|
||||
|
||||
if (multiSelect) {
|
||||
const check = document.createElement("span");
|
||||
check.classList.add("checkmark");
|
||||
check.setAttribute("aria-hidden", "true");
|
||||
li.append(check);
|
||||
}
|
||||
|
||||
const labelEl = document.createElement("span");
|
||||
labelEl.classList.add("label");
|
||||
labelEl.textContent = option.label;
|
||||
li.append(labelEl);
|
||||
|
||||
if (selected && multiSelect) {
|
||||
li.title = game.i18n.localize("l5r5e.multiselect.already_in_filter");
|
||||
} else if (disabled && option.tooltip) {
|
||||
li.title = option.tooltip;
|
||||
}
|
||||
|
||||
if (!disabled) {
|
||||
li.addEventListener("mouseenter", () => {
|
||||
for (const element of this.#optionElements) {
|
||||
element.classList.remove("active");
|
||||
}
|
||||
this.#activeIndex = this.#optionElements.indexOf(li);
|
||||
li.classList.add("active");
|
||||
});
|
||||
li.addEventListener("mouseleave", () => {
|
||||
li.classList.remove("active");
|
||||
this.#activeIndex = -1;
|
||||
});
|
||||
li.addEventListener("mousedown", (event) => {
|
||||
event.preventDefault(); // keep focus on the search input
|
||||
this.#pick(option);
|
||||
});
|
||||
}
|
||||
|
||||
this.#optionElements.push(li);
|
||||
return li;
|
||||
}
|
||||
|
||||
#pick(option) {
|
||||
this._onDropdownPick(option);
|
||||
if (multiSelect) {
|
||||
// Stay open — re-render so checkmarks reflect the new state.
|
||||
this.#renderOptions(this.#filter(this.#searchInput.value));
|
||||
} else {
|
||||
this.#closeDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
#moveHighlight(direction) {
|
||||
if (!this.#optionElements.length) {
|
||||
return;
|
||||
}
|
||||
const prev = this.#optionElements[this.#activeIndex];
|
||||
if (prev) {
|
||||
prev.classList.remove("active");
|
||||
}
|
||||
|
||||
this.#activeIndex = Math.max(
|
||||
-1,
|
||||
Math.min(this.#optionElements.length - 1, this.#activeIndex + direction)
|
||||
);
|
||||
|
||||
const next = this.#optionElements[this.#activeIndex];
|
||||
if (next) {
|
||||
next.classList.add("active");
|
||||
next.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
}
|
||||
|
||||
#syncOffset() {
|
||||
if (!this.#searchInput || !this.#list) {
|
||||
return;
|
||||
}
|
||||
const offset = this.#searchInput.offsetLeft;
|
||||
this.#list.style.left = `${offset}px`;
|
||||
this.#list.style.right = `-${offset}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles click-to-toggle behaviour.
|
||||
*
|
||||
* Three cases:
|
||||
* 1. Dropdown is open → close it. preventDefault stops blur firing, which
|
||||
* would otherwise trigger a second close via #onBlur.
|
||||
* 2. Dropdown is closed AND input is already focused → open immediately.
|
||||
* In this case focus will NOT fire again (input never blurred after the
|
||||
* previous preventDefault), so we must open here rather than in #onFocus.
|
||||
* 3. Dropdown is closed AND input is not yet focused → do nothing; the
|
||||
* browser will fire focus naturally and #onFocus will open the dropdown.
|
||||
*/
|
||||
#onMouseDown(event) {
|
||||
this.#wasOpenOnFocus = this.#open;
|
||||
if (this.#open) {
|
||||
event.preventDefault();
|
||||
this.#closeDropdown();
|
||||
} else if (document.activeElement === this.#searchInput) {
|
||||
// Case 2: already focused, focus won't re-fire — open directly.
|
||||
this.#openDropdown(this.#searchInput.value);
|
||||
}
|
||||
}
|
||||
|
||||
#onFocus(event) {
|
||||
// Only open if mousedown didn't already handle it (cases 1 & 2 above).
|
||||
if (!this.#wasOpenOnFocus && document.activeElement === this.#searchInput) {
|
||||
this.#openDropdown(this.#searchInput.value);
|
||||
}
|
||||
}
|
||||
|
||||
#onInput(event) {
|
||||
this.#debouncedOpen(this.#searchInput.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the dropdown when focus genuinely leaves the component.
|
||||
*
|
||||
* event.relatedTarget is the element that is RECEIVING focus. If it is
|
||||
* inside our host element (e.g. the clear button, a chip remove span that
|
||||
* managed to steal focus) we leave the dropdown open. Only when focus moves
|
||||
* completely outside do we close — and we do so synchronously, so that
|
||||
* _onDropdownClosed fires its change event BEFORE the browser hands control
|
||||
* to whatever the user clicked. This eliminates the 100 ms race where Foundry
|
||||
* could read stale FormData between the blur and a deferred close.
|
||||
*/
|
||||
#onBlur(event) {
|
||||
if (this.contains(event.relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
this.#closeDropdown();
|
||||
}
|
||||
|
||||
#onKeydown(event) {
|
||||
if (!this.#open) {
|
||||
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
this.#openDropdown(this.#searchInput.value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (event.key) {
|
||||
case "ArrowDown": {
|
||||
event.preventDefault();
|
||||
this.#moveHighlight(1);
|
||||
} break;
|
||||
case "ArrowUp": {
|
||||
event.preventDefault();
|
||||
this.#moveHighlight(-1);
|
||||
}break;
|
||||
case "Enter": {
|
||||
event.preventDefault();
|
||||
if (this.#activeIndex >= 0) {
|
||||
const li = this.#optionElements[this.#activeIndex];
|
||||
const option = this._getDropdownOptions().find(option => option.value === li.dataset.value);
|
||||
if (option) {
|
||||
this.#pick(option);
|
||||
}
|
||||
} else {
|
||||
this.#closeDropdown();
|
||||
}
|
||||
} break;
|
||||
case "Escape": {
|
||||
event.preventDefault();
|
||||
this.#closeDropdown();
|
||||
} break;
|
||||
case "Tab": {
|
||||
this.#closeDropdown();
|
||||
} break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,301 +1,424 @@
|
||||
import { DropdownMixin } from "./l5r5e-dropdown-mixin.js";
|
||||
|
||||
const { AbstractMultiSelectElement } = foundry.applications.elements;
|
||||
|
||||
/**
|
||||
* Provide a multi-select workflow using a select element as the input mechanism.
|
||||
* It is a expanded copy of the HTMLMultiselect with support for disabling options
|
||||
* and a clear all button. Also have support for hover-over information using titlea
|
||||
* A custom `<l5r5e-multi-select>` form element providing Select2-style chip multi-selection.
|
||||
*
|
||||
* @example Multi-Select HTML Markup
|
||||
* ```html
|
||||
* <l5r5e-multi-select name="select-many-things">
|
||||
* <optgroup label="Basic Options">
|
||||
* <option value="foo">Foo</option>
|
||||
* <option value="bar">Bar</option>
|
||||
* <option value="baz">Baz</option>
|
||||
* </optgroup>
|
||||
* <optgroup label="Advanced Options">
|
||||
* <option value="fizz">Fizz</option>
|
||||
* <option value="buzz">Buzz</option>
|
||||
* </optgroup>
|
||||
* Stores **multiple string values** from a fixed option list, shown as removable chips inside
|
||||
* the input box. A live-search input filters the dropdown as the user types. Use this when a
|
||||
* field holds an unordered collection of values (e.g. a set of skills, tags, or abilities).
|
||||
* For storing a single value — predefined or free-text — use {@link L5R5eHtmlComboBoxElement} instead.
|
||||
*
|
||||
* The element's `value` getter returns a comma-separated string (e.g. `"fire,water"`),
|
||||
* which is what `FormData` will read on submission. `_getValue()` returns a plain Array,
|
||||
* which is what `FormDataExtended` will use.
|
||||
*
|
||||
* Pre-selection on render is handled via the `value` attribute on the element — NOT via
|
||||
* `{{selectOptions selected=...}}`, which cannot handle comma-separated strings. Use
|
||||
* `{{selectOptions}}` without `selected` purely to render the available options, and let
|
||||
* the `value` attribute drive pre-selection. Since `getAttribute()` always returns a string,
|
||||
* passing a `Set` or `Array` via Handlebars will not work correctly — always pass a
|
||||
* comma-separated string to `value=`.
|
||||
*
|
||||
* Prefer {@link L5r5eSetField} + `{{formGroup}}` when wiring this into a DataModel — the
|
||||
* field handles the full round-trip automatically.
|
||||
*
|
||||
* @example
|
||||
* ```hbs
|
||||
* {{!-- Use value= (comma-separated string) for pre-selection, not selectOptions selected= --}}
|
||||
* <l5r5e-multi-select name="elements" value="{{data.elements}}">
|
||||
* {{selectOptions choices localize=true}}
|
||||
* </l5r5e-multi-select>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* // Static factory — use only when building outside of Foundry's field/template system:
|
||||
* const el = L5r5eHtmlMultiSelectElement.create({
|
||||
* name: "elements",
|
||||
* options: [{ value: "fire", label: "Fire" }, { value: "water", label: "Water" }],
|
||||
* value: "fire,water", // comma-separated pre-selection
|
||||
* });
|
||||
* form.appendChild(el);
|
||||
*
|
||||
* // Reading the value back:
|
||||
* el.value; // "fire,water" — comma-separated string, compatible with FormData
|
||||
* el._getValue(); // ["fire","water"] — array, compatible with FormDataExtended
|
||||
*/
|
||||
export class L5r5eHtmlMultiSelectElement extends AbstractMultiSelectElement {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.#setup();
|
||||
}
|
||||
|
||||
export class L5r5eHtmlMultiSelectElement extends DropdownMixin(
|
||||
AbstractMultiSelectElement,
|
||||
{ multiSelect: true, debounceMs: 150 }
|
||||
) {
|
||||
/** @override */
|
||||
static tagName = "l5r5e-multi-select";
|
||||
|
||||
/**
|
||||
* A select element used to choose options.
|
||||
* @type {HTMLSelectElement}
|
||||
*/
|
||||
#select;
|
||||
/** @type {HTMLDivElement} — outer box containing chips, input, clear button */
|
||||
#selectionBox;
|
||||
|
||||
/** @type {HTMLDivElement} — chips are injected here */
|
||||
#chipList;
|
||||
|
||||
/** @type {HTMLSpanElement} — auto-sizing wrapper around the search input */
|
||||
#inputSizer;
|
||||
|
||||
/** @type {HTMLButtonElement} — trailing clear-all button */
|
||||
#clearButton;
|
||||
|
||||
/** @type {Set<string>} */
|
||||
#disabledValues = new Set();
|
||||
|
||||
/** @type {Map<string, string>} */
|
||||
#tooltips = new Map();
|
||||
|
||||
|
||||
/**
|
||||
* A display element which lists the chosen options.
|
||||
* @type {HTMLDivElement}
|
||||
* Returns a comma-separated string
|
||||
* FormData reads this via field.value.
|
||||
* @override
|
||||
*/
|
||||
#tags;
|
||||
get value() {
|
||||
return Array.from(this._value).join(",");
|
||||
}
|
||||
|
||||
/** @override */
|
||||
set value(val) {
|
||||
this._value.clear();
|
||||
const values = Array.isArray(val) ? val : String(val).split(",").filter(Boolean);
|
||||
for (const v of values) {
|
||||
this._value.add(v);
|
||||
}
|
||||
this._internals.setFormValue(this.value);
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* A button element which clear all the options.
|
||||
* @type {HTMLButtonElement}
|
||||
* Return an array so FormDataExtended.object[name] matches Foundry's own
|
||||
* HTMLMultiSelectElement — both field.value (string) and .object (array) are correct.
|
||||
* @override
|
||||
* @protected
|
||||
*/
|
||||
#clearAll;
|
||||
_getValue() {
|
||||
return Array.from(this._value);
|
||||
}
|
||||
|
||||
/**
|
||||
* A Set containing the values that should always be disabled.
|
||||
* @type {Set}
|
||||
* Accept either an array or comma-separated string when Foundry calls _setValue().
|
||||
* @override
|
||||
* @protected
|
||||
*/
|
||||
#disabledValues;
|
||||
_setValue(val) {
|
||||
const values = Array.isArray(val) ? val : String(val).split(",").filter(Boolean);
|
||||
if (values.some(v => v && !(v in this._choices))) {
|
||||
throw new Error("The values assigned to a multi-select element must all be valid options.");
|
||||
}
|
||||
this._value.clear();
|
||||
for (const v of values) {
|
||||
this._value.add(v);
|
||||
}
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_initialize() {
|
||||
super._initialize(); // fills this._choices, this._value, this._options
|
||||
for (const option of this.querySelectorAll("option")) {
|
||||
if (option.disabled)
|
||||
this.#disabledValues.add(option.value);
|
||||
if (option.title)
|
||||
this.#tooltips.set(option.value, option.title);
|
||||
}
|
||||
if (this.hasAttribute("value")) {
|
||||
this._setValue(this.getAttribute("value"));
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// We will call initialize twice (one in the parent constructor) then one in #setup
|
||||
// required since when we want to build the elements we should to an initialize first
|
||||
// and we cannot override _initialize since we don't have access to #disabledValues there
|
||||
#setup() {
|
||||
super._initialize();
|
||||
this.#disabledValues = new Set();
|
||||
for (const option of this.querySelectorAll("option")) {
|
||||
if (option.value === "") {
|
||||
option.label = game.i18n.localize("l5r5e.multiselect.empty_tag");
|
||||
this._choices[option.value] = game.i18n.localize("l5r5e.multiselect.empty_tag");
|
||||
}
|
||||
if (option.disabled) {
|
||||
this.#disabledValues.add(option.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Element Lifecycle */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_buildElements() {
|
||||
this.#setup();
|
||||
|
||||
// Create select element
|
||||
this.#select = this._primaryInput = document.createElement("select");
|
||||
this.#select.insertAdjacentHTML("afterbegin", `<option id="l5r5e-multiselect-placeholder" value="" disabled selected hidden>${game.i18n.localize("l5r5e.multiselect.placeholder")}</option>`);
|
||||
this.#select.append(...this._options);
|
||||
this.#select.disabled = !this.editable;
|
||||
|
||||
// Create a div element for display
|
||||
this.#tags = document.createElement("div");
|
||||
this.#tags.className = "tags input-element-tags";
|
||||
|
||||
// Create a clear all button
|
||||
this.#clearAll = document.createElement("button");
|
||||
this.#clearAll.textContent = "X";
|
||||
return [this.#select, this.#clearAll, this.#tags];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_refresh() {
|
||||
// Update the displayed tags
|
||||
const tags = Array.from(this._value).map(id => {
|
||||
return foundry.applications.elements.HTMLStringTagsElement.renderTag(id, this._choices[id], this.editable);
|
||||
// Ask mixin to build <input> + <ul>, then re-home them into our structure.
|
||||
const mixinWrapper = this._buildDropdownElements({
|
||||
placeholder: this.getAttribute("placeholder")
|
||||
?? game.i18n.localize("l5r5e.multiselect.placeholder"),
|
||||
});
|
||||
this.#tags.replaceChildren(...tags);
|
||||
const searchInput = mixinWrapper.querySelector("input.input");
|
||||
const dropdownList = mixinWrapper.querySelector("ul.dropdown");
|
||||
|
||||
// Figure out if we are overflowing the tag div.
|
||||
if($(this.#tags).css("max-height")) {
|
||||
const numericMaxHeight = parseInt($(this.#tags).css("max-height"), 10);
|
||||
if(numericMaxHeight) {
|
||||
if($(this.#tags).prop("scrollHeight") > numericMaxHeight) {
|
||||
this.#tags.classList.add("overflowing");
|
||||
}
|
||||
else {
|
||||
this.#tags.classList.remove("overflowing");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Selection box
|
||||
this.#selectionBox = document.createElement("div");
|
||||
this.#selectionBox.classList.add("selection-box");
|
||||
|
||||
// Disable selected options
|
||||
const hideDisabled = game.settings.get(CONFIG.l5r5e.namespace, "compendium-hide-disabled-sources");
|
||||
for (const option of this.#select) {
|
||||
if (this._value.has(option.value)) {
|
||||
option.disabled = true;
|
||||
option.title = game.i18n.localize("l5r5e.multiselect.already_in_filter");
|
||||
continue;
|
||||
}
|
||||
if (this.#disabledValues.has(option.value)) {
|
||||
option.disabled = true;
|
||||
option.hidden = hideDisabled;
|
||||
continue;
|
||||
}
|
||||
option.disabled = false;
|
||||
option.removeAttribute("title");
|
||||
}
|
||||
}
|
||||
// Chip list
|
||||
this.#chipList = document.createElement("div");
|
||||
this.#chipList.classList.add("chip-list");
|
||||
|
||||
/* -------------------------------------------- */
|
||||
// Auto-sizing sizer — CSS grid trick: ::after mirrors data-value, input shares the cell
|
||||
this.#inputSizer = document.createElement("span");
|
||||
this.#inputSizer.classList.add("input-sizer");
|
||||
this.#inputSizer.dataset.value = "";
|
||||
this.#inputSizer.append(searchInput);
|
||||
|
||||
// Clear-all button
|
||||
this.#clearButton = document.createElement("button");
|
||||
this.#clearButton.type = "button";
|
||||
this.#clearButton.classList.add("clear-btn");
|
||||
this.#clearButton.setAttribute("aria-label",
|
||||
game.i18n.localize("l5r5e.multiselect.clear_all") ?? "Clear all");
|
||||
this.#clearButton.textContent = "×";
|
||||
this.#clearButton.hidden = true;
|
||||
|
||||
this.#selectionBox.append(this.#chipList, this.#inputSizer, this.#clearButton);
|
||||
|
||||
// Container: selection box + dropdown must share the same positioned ancestor.
|
||||
const container = document.createElement("div");
|
||||
container.classList.add("multi-select-container");
|
||||
container.append(this.#selectionBox, dropdownList);
|
||||
|
||||
this._primaryInput = searchInput;
|
||||
return [container];
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_activateListeners() {
|
||||
this.#select.addEventListener("change", this.#onChangeSelect.bind(this));
|
||||
this.#clearAll.addEventListener("click", this.#onClickClearAll.bind(this));
|
||||
this.#tags.addEventListener("click", this.#onClickTag.bind(this));
|
||||
this._activateDropdownListeners();
|
||||
const signal = this.abortSignal;
|
||||
|
||||
this.#tags.addEventListener("mouseleave", this.#onMouseLeave.bind(this));
|
||||
this.#selectionBox.addEventListener("mousedown", this.#onBoxMouseDown.bind(this), { signal });
|
||||
this.#chipList.addEventListener("click", this.#onChipClick.bind(this), { signal });
|
||||
this.#clearButton.addEventListener("click", this.#onClearAll.bind(this), { signal });
|
||||
// stop the clear button from opening the selection box when pressing it
|
||||
this.#clearButton.addEventListener("mousedown", (event) => {event.preventDefault(); event.stopPropagation();}, {signal});
|
||||
this._dropdownInput.addEventListener("input", () => this.#updateInputSizer(), { signal });
|
||||
}
|
||||
|
||||
#onMouseLeave(event) {
|
||||
// Figure out if we are overflowing the tag div.
|
||||
if($(this.#tags).css("max-height")) {
|
||||
const numericMaxHeight = parseInt($(this.#tags).css("max-height"), 10);
|
||||
|
||||
if($(this.#tags).prop("scrollHeight") > numericMaxHeight) {
|
||||
this.#tags.classList.add("overflowing");
|
||||
}
|
||||
else {
|
||||
this.#tags.classList.remove("overflowing");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle changes to the Select input, marking the selected option as a chosen value.
|
||||
* @param {Event} event The change event on the select element
|
||||
*/
|
||||
#onChangeSelect(event) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
const select = event.currentTarget;
|
||||
if (select.valueIndex === 0)
|
||||
return; // Ignore placeholder
|
||||
this.select(select.value);
|
||||
select.value = "";
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle click events on a tagged value, removing it from the chosen set.
|
||||
* @param {PointerEvent} event The originating click event on a chosen tag
|
||||
*/
|
||||
#onClickTag(event) {
|
||||
event.preventDefault();
|
||||
if (!event.target.classList.contains("remove"))
|
||||
return;
|
||||
if (!this.editable)
|
||||
return;
|
||||
const tag = event.target.closest(".tag");
|
||||
this.unselect(tag.dataset.key);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle clickling the clear all button
|
||||
* @param {Event} event The originating click event on the clear all button
|
||||
*/
|
||||
#onClickClearAll(event) {
|
||||
event.preventDefault();
|
||||
var _this = this;
|
||||
$(this.#tags).children().each(function () {
|
||||
_this.unselect($(this).data("key"));
|
||||
})
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/** @override */
|
||||
_toggleDisabled(disabled) {
|
||||
this.#select.toggleAttribute("disabled", disabled);
|
||||
this._toggleDropdownDisabled(disabled);
|
||||
|
||||
if (this.#selectionBox) {
|
||||
this.#selectionBox.classList.toggle("disabled", disabled);
|
||||
}
|
||||
|
||||
if (this.#chipList) {
|
||||
this._refresh(); // re-render chips so × appears/disappears
|
||||
}
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_refresh() {
|
||||
const values = Array.from(this._value);
|
||||
this._internals.setFormValue(values.length ? values.join(",") : "");
|
||||
|
||||
this.#renderChips(values);
|
||||
|
||||
// Clear button: only visible when editable and something is selected.
|
||||
if (this.#clearButton) {
|
||||
this.#clearButton.hidden = (!this.editable || values.length === 0);
|
||||
}
|
||||
|
||||
this.#updateInputSizer();
|
||||
this._dropdownRefresh();
|
||||
}
|
||||
|
||||
/** @param {string[]} values */
|
||||
#renderChips(values) {
|
||||
if (!this.#chipList){
|
||||
return
|
||||
}
|
||||
this.#chipList.replaceChildren(...values.map(id => this.#buildChip(id)));
|
||||
}
|
||||
|
||||
/** @param {string} id */
|
||||
#buildChip(id) {
|
||||
const chip = document.createElement("span");
|
||||
chip.classList.add("chip");
|
||||
chip.dataset.key = id;
|
||||
|
||||
const label = document.createElement("span");
|
||||
label.classList.add("chip-label");
|
||||
label.textContent = this._choices[id] ?? id;
|
||||
chip.append(label);
|
||||
|
||||
// Only add × when the element is editable (not disabled, not readonly).
|
||||
if (this.editable) {
|
||||
const remove = document.createElement("span");
|
||||
remove.classList.add("chip-remove");
|
||||
remove.setAttribute("aria-label", `Remove ${this._choices[id] ?? id}`);
|
||||
remove.setAttribute("aria-hidden", "true");
|
||||
remove.textContent = "×";
|
||||
chip.append(remove);
|
||||
}
|
||||
return chip;
|
||||
}
|
||||
|
||||
/** Mirror typed text into the sizer span so CSS sizes the input correctly. */
|
||||
#updateInputSizer() {
|
||||
if (!this.#inputSizer || !this._dropdownInput)
|
||||
return;
|
||||
|
||||
const input = this._dropdownInput;
|
||||
const text = input.value || input.placeholder || "";
|
||||
this.#inputSizer.dataset.value = text;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_getDropdownOptions() {
|
||||
const makeOption = (option, group = null) => ({
|
||||
value: option.value,
|
||||
label: this._choices[option.value] ?? option.innerText,
|
||||
group,
|
||||
disabled: this.#disabledValues.has(option.value),
|
||||
tooltip: this.#tooltips.get(option.value) ?? "",
|
||||
});
|
||||
|
||||
return this._options.flatMap(child => {
|
||||
if (child instanceof HTMLOptGroupElement) {
|
||||
return [...child.querySelectorAll("option")]
|
||||
.filter(option => option.value)
|
||||
.map(option => makeOption(option, child.label));
|
||||
}
|
||||
if (child instanceof HTMLOptionElement && child.value) {
|
||||
return makeOption(child);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_isOptionSelected(value) {
|
||||
return this._value.has(value);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_onDropdownPick(option) {
|
||||
const inValue = this._value.has(option.value);
|
||||
const inChoices = option.value in this._choices;
|
||||
if(!(inValue || inChoices))
|
||||
return;
|
||||
|
||||
if (inValue) {
|
||||
this._value.delete(option.value);
|
||||
}
|
||||
else if(inChoices) {
|
||||
this._value.add(option.value);
|
||||
}
|
||||
|
||||
this._internals.setFormValue(this.value);
|
||||
this._refresh();
|
||||
this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
|
||||
}
|
||||
|
||||
#onBoxMouseDown(event) {
|
||||
// Fully block interaction when not editable.
|
||||
if (!this.editable) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (event.target.classList.contains("chip-remove"))
|
||||
return;
|
||||
if (event.target === this._dropdownInput)
|
||||
return;
|
||||
|
||||
event.preventDefault();
|
||||
this._dropdownInput?.focus();
|
||||
}
|
||||
|
||||
#onChipClick(event) {
|
||||
if (!event.target.classList.contains("chip-remove") || !this.editable)
|
||||
return;
|
||||
|
||||
const chip = event.target.closest(".chip");
|
||||
if (!chip)
|
||||
return;
|
||||
|
||||
this._value.delete(chip.dataset.key);
|
||||
this._internals.setFormValue(this.value);
|
||||
this._refresh();
|
||||
this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
|
||||
this._dropdownInput?.focus();
|
||||
}
|
||||
|
||||
#onClearAll(event) {
|
||||
event.preventDefault();
|
||||
if (!this.editable)
|
||||
return;
|
||||
|
||||
this._value.clear();
|
||||
this._internals.setFormValue("");
|
||||
this._refresh();
|
||||
this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Static Factory */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a HTML_l5r5e_MultiSelectElement using provided configuration data.
|
||||
* @param {FormInputConfig<string[]> & Omit<SelectInputConfig, "blank">} config
|
||||
* @returns {L5r5eHtmlMultiSelectElement}
|
||||
*/
|
||||
static create(config) {
|
||||
// Foundry creates either a select with tag multi-select or multi-checkboxes. We want a l5r5e-multi-select
|
||||
// Copied the implementation from foundry.applications.fields.createMultiSelectInput with our required changes.
|
||||
const groups = prepareSelectOptionGroups(config);
|
||||
const element = document.createElement(L5r5eHtmlMultiSelectElement.tagName);
|
||||
element.name = config.name;
|
||||
foundry.applications.fields.setInputAttributes(element, config);
|
||||
if (config.hideDisabledOptions) {
|
||||
element.toggleAttribute("hidedisabledoptions", true);
|
||||
}
|
||||
|
||||
//Setup the HTML
|
||||
const select = document.createElement(L5r5eHtmlMultiSelectElement.tagName);
|
||||
select.name = config.name;
|
||||
foundry.applications.fields.setInputAttributes(select, config);
|
||||
for (const group_entry of groups) {
|
||||
let parent = select;
|
||||
if (group_entry.group) {
|
||||
parent = _appendOptgroupHtml(group_entry.group, select);
|
||||
for (const groupEntry of groups) {
|
||||
let parent = element;
|
||||
if (groupEntry.group) {
|
||||
parent = _appendOptgroup(groupEntry.group, element);
|
||||
}
|
||||
for (const option_entry of group_entry.options) {
|
||||
_appendOptionHtml(option_entry, parent);
|
||||
for (const groupOption of groupEntry.options){
|
||||
_appendOption(groupOption, parent);
|
||||
}
|
||||
}
|
||||
return select;
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
/** Stolen from foundry.applications.fields.prepareSelectOptionGroups: Needed to add support for tooltips
|
||||
*
|
||||
*/
|
||||
/* -------------------------------------------- */
|
||||
/* Module Helpers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
function prepareSelectOptionGroups(config) {
|
||||
const result = foundry.applications.fields.prepareSelectOptionGroups(config);
|
||||
|
||||
// Disable options based on input
|
||||
config.options.filter((option) => option?.disabled || option?.tooltip).forEach((SpecialOption) => {
|
||||
result.forEach((group) => {
|
||||
group.options.forEach((option) => {
|
||||
if (SpecialOption.value === option.value) {
|
||||
option.disabled = SpecialOption.disabled;
|
||||
option.tooltip = SpecialOption?.tooltip;
|
||||
config.options.filter(option => option?.disabled || option?.tooltip).forEach(special => {
|
||||
result.forEach(group => {
|
||||
group.options.forEach(groupOption => {
|
||||
if (groupOption.value === special.value) {
|
||||
groupOption.disabled = special.disabled;
|
||||
groupOption.tooltip = special.tooltip;
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Stolen from foundry.applications.fields
|
||||
* Create and append an optgroup element to a parent select.
|
||||
* @param {string} label
|
||||
* @param {HTMLSelectElement} parent
|
||||
* @returns {HTMLOptGroupElement}
|
||||
* @internal
|
||||
*/
|
||||
function _appendOptgroupHtml(label, parent) {
|
||||
const optgroup = document.createElement("optgroup");
|
||||
optgroup.label = label;
|
||||
parent.appendChild(optgroup);
|
||||
return optgroup;
|
||||
function _appendOptgroup(label, parent) {
|
||||
const element = document.createElement("optgroup");
|
||||
element.label = label;
|
||||
parent.appendChild(element);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/** Stolen from foundry.applications.fields
|
||||
* Create and append an option element to a parent select or optgroup.
|
||||
* @param {FormSelectOption} option
|
||||
* @param {HTMLSelectElement|HTMLOptGroupElement} parent
|
||||
* @internal
|
||||
*/
|
||||
function _appendOptionHtml(option, parent) {
|
||||
function _appendOption(option, parent) {
|
||||
const { value, label, selected, disabled, rule, tooltip } = option;
|
||||
if ((value !== undefined) && (label !== undefined)) {
|
||||
const option_html = document.createElement("option");
|
||||
option_html.value = value;
|
||||
option_html.innerText = label;
|
||||
if (value !== undefined && label !== undefined) {
|
||||
const element = document.createElement("option");
|
||||
element.value = value;
|
||||
element.innerText = label;
|
||||
if (selected) {
|
||||
option_html.toggleAttribute("selected", true);
|
||||
element.toggleAttribute("selected", true);
|
||||
}
|
||||
if (disabled) {
|
||||
option_html.toggleAttribute("disabled", true);
|
||||
element.toggleAttribute("disabled", true);
|
||||
}
|
||||
if (tooltip) {
|
||||
option_html.setAttribute("title", tooltip);
|
||||
element.setAttribute("title", tooltip);
|
||||
}
|
||||
parent.appendChild(option_html);
|
||||
parent.appendChild(element);
|
||||
}
|
||||
if (rule) {
|
||||
parent.insertAdjacentHTML("beforeend", "<hr>");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { L5r5eSetField } from "./data/l5r5e-setfield.js";
|
||||
import { TacticalGridSettingsL5R5E } from "./settings/tactical-grid-settings.js"
|
||||
|
||||
/**
|
||||
* Custom system settings register
|
||||
@@ -236,4 +237,28 @@ export const RegisterSettings = function () {
|
||||
default: [],
|
||||
onChange: () => game.l5r5e.HelpersL5r5e.refreshLocalAndSocket("l5r5e-gm-monitor"),
|
||||
});
|
||||
|
||||
/* -------------------------------------- */
|
||||
/* Grid Settings (GM only) */
|
||||
/* -------------------------------------- */
|
||||
// UI Configuration
|
||||
game.settings.register(CONFIG.l5r5e.namespace, "tactical-grid-settings-world", {
|
||||
scope: "world",
|
||||
config: false,
|
||||
type: TacticalGridSettingsL5R5E.worldSchema,
|
||||
});
|
||||
|
||||
game.settings.register(CONFIG.l5r5e.namespace, "tactical-grid-settings-client", {
|
||||
scope: "client",
|
||||
config: false,
|
||||
type: TacticalGridSettingsL5R5E.clientSchema,
|
||||
});
|
||||
|
||||
game.settings.registerMenu(CONFIG.l5r5e.namespace, "tactical-grid-settings", {
|
||||
name: "l5r5e.tactical_grid.settings.title",
|
||||
label: "l5r5e.tactical_grid.settings.label",
|
||||
hint: "l5r5e.tactical_grid.settings.hint",
|
||||
icon: "fa-solid fa-table-layout",
|
||||
type: TacticalGridSettingsL5R5E
|
||||
});
|
||||
};
|
||||
|
||||
351
system/scripts/settings/tactical-grid-settings.js
Normal file
351
system/scripts/settings/tactical-grid-settings.js
Normal file
@@ -0,0 +1,351 @@
|
||||
const HandlebarsApplicationMixin = foundry.applications.api.HandlebarsApplicationMixin;
|
||||
const ApplicationV2 = foundry.applications.api.ApplicationV2;
|
||||
const fields = foundry.data.fields;
|
||||
|
||||
/**
|
||||
*
|
||||
* @typedef {Object} RangeBand
|
||||
* @property {number} start
|
||||
*
|
||||
* @typedef {Object} ClientRangeBand
|
||||
* @property {string} color
|
||||
* @property {number} alpha
|
||||
*
|
||||
* @typedef {Object} WorldSettings
|
||||
* @property {boolean} enabled
|
||||
* @property {Record<number, RangeBand>} ranges - Indexed 0-6
|
||||
*
|
||||
* @typedef {Object} ClientSettings
|
||||
* @property {Record<number, ClientRangeBand>} ranges - Indexed 0-6
|
||||
*/
|
||||
|
||||
export class TacticalGridSettingsL5R5E extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
|
||||
/** @inheritDoc */
|
||||
static DEFAULT_OPTIONS = {
|
||||
id: "tactical-grid-settings",
|
||||
tag: "form",
|
||||
classes: [""], // We could add l5r here but that would add styling that is not matching the default settings menu
|
||||
window: {
|
||||
title: "l5r5e.tactical_grid.settings.title",
|
||||
contentClasses: ["standard-form"]
|
||||
},
|
||||
form: {
|
||||
closeOnSubmit: true,
|
||||
handler: TacticalGridSettingsL5R5E.#onSubmit
|
||||
},
|
||||
position: { width: 540 },
|
||||
actions: {
|
||||
reset: TacticalGridSettingsL5R5E.#onReset
|
||||
}
|
||||
};
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
form: {
|
||||
template: "systems/l5r5e/templates/" + "settings/tactical-grid-settings.html",
|
||||
scrollable: [""],
|
||||
},
|
||||
footer: {
|
||||
template: "templates/generic/form-footer.hbs"
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a SchemaField defining a world range band.
|
||||
* @param {{start: number}} initial - Initial range values.
|
||||
* `start` must be ≥ 0.
|
||||
* * @returns {SchemaField} A schema field containing a 'start' field
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
static #createWorldRangeBandSchema(initial) {
|
||||
return new fields.SchemaField({
|
||||
start: new fields.NumberField({ initial: initial.start, label: "l5r5e.tactical_grid.settings.world.start", min: 0, max:Infinity, nullable: false, required: true, gmOnly: true})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a SchemaField defining a client range band.
|
||||
* @param {{color: string, alpha: number}} initial - Initial range band values.
|
||||
* `color` should be a valid CSS color string and `alpha` a valid alpha value.
|
||||
* @returns {SchemaField} A schema field containing `color` and `alpha` fields.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
static #createClientRangeBandSchema(initial) {
|
||||
return new fields.SchemaField({
|
||||
color: new fields.ColorField({initial: initial.color, label: "l5r5e.tactical_grid.settings.client.color", required: true}),
|
||||
alpha: new fields.AlphaField({initial: initial.alpha, label: "l5r5e.tactical_grid.settings.client.alpha", required: true}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined Foundry VTT settings schema representing both:
|
||||
* - **World (GM-controlled)** configuration
|
||||
* - **Client (per-user)** visual configuration
|
||||
*
|
||||
* This variable serves as a single source-of-truth definition for the module’s
|
||||
* tactical grid settings structure, including field types, defaults, labels, and
|
||||
* validation rules for world ranges.
|
||||
* @private
|
||||
*/
|
||||
static #schema = {
|
||||
world: new fields.SchemaField({
|
||||
enabled: new fields.BooleanField({ initial: true, label: "l5r5e.tactical_grid.settings.world.enabled", hint: "l5r5e.tactical_grid.settings.world.enabled_hint"}),
|
||||
ranges: new fields.SchemaField({
|
||||
0: TacticalGridSettingsL5R5E.#createWorldRangeBandSchema({ start: 0}),
|
||||
1: TacticalGridSettingsL5R5E.#createWorldRangeBandSchema({ start: 1}),
|
||||
2: TacticalGridSettingsL5R5E.#createWorldRangeBandSchema({ start: 2}),
|
||||
3: TacticalGridSettingsL5R5E.#createWorldRangeBandSchema({ start: 3}),
|
||||
4: TacticalGridSettingsL5R5E.#createWorldRangeBandSchema({ start: 6}),
|
||||
5: TacticalGridSettingsL5R5E.#createWorldRangeBandSchema({ start: 10}),
|
||||
6: TacticalGridSettingsL5R5E.#createWorldRangeBandSchema({ start: 15})
|
||||
})
|
||||
}, {
|
||||
validate: TacticalGridSettingsL5R5E.#validateWorldRangeConfiguration
|
||||
}),
|
||||
client: new fields.SchemaField({
|
||||
ranges: new fields.SchemaField({
|
||||
0: TacticalGridSettingsL5R5E.#createClientRangeBandSchema({color: "#00FFFF", alpha: 0.5}),
|
||||
1: TacticalGridSettingsL5R5E.#createClientRangeBandSchema({color: "#FF00FF", alpha: 0.5}),
|
||||
2: TacticalGridSettingsL5R5E.#createClientRangeBandSchema({color: "#FFFF00", alpha: 0.5}),
|
||||
3: TacticalGridSettingsL5R5E.#createClientRangeBandSchema({color: "#0000FF", alpha: 0.5}),
|
||||
4: TacticalGridSettingsL5R5E.#createClientRangeBandSchema({color: "#7FFF00", alpha: 0.5}),
|
||||
5: TacticalGridSettingsL5R5E.#createClientRangeBandSchema({color: "#4B0082", alpha: 0.5}),
|
||||
6: TacticalGridSettingsL5R5E.#createClientRangeBandSchema({color: "#FF8800", alpha: 0.5})
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
* Exposes the **world (GM-controlled)** portion of the tactical grid settings schema.
|
||||
* @return {SchemaField}
|
||||
*/
|
||||
static get worldSchema() {
|
||||
return TacticalGridSettingsL5R5E.#schema.world;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes the **client (per-user visual)** portion of the tactical grid settings schema.
|
||||
* @return {SchemaField}
|
||||
*/
|
||||
static get clientSchema() {
|
||||
return TacticalGridSettingsL5R5E.#schema.client;
|
||||
}
|
||||
|
||||
/** Holds a mutable copy of the tactical grid settings so the form can operate on current values without altering the schema. */
|
||||
static #setting = null;
|
||||
|
||||
|
||||
/** @override ApplicationV2 */
|
||||
async _prepareContext(options) {
|
||||
if (options.isFirstRender) {
|
||||
const client = game.settings.get(CONFIG.l5r5e.namespace, "tactical-grid-settings-client");
|
||||
const world = game.settings.get(CONFIG.l5r5e.namespace, "tactical-grid-settings-world");
|
||||
TacticalGridSettingsL5R5E.#setting = foundry.utils.deepClone({client: client, world: world});
|
||||
}
|
||||
|
||||
// Pre-process range bands for easier template access
|
||||
const rangeBands = Object.entries(this.constructor.worldSchema.fields.ranges.fields).map(([index, field]) => ({
|
||||
index: Number(index),
|
||||
worldField: field,
|
||||
clientFields: this.constructor.clientSchema.fields.ranges.fields[index],
|
||||
worldValue: TacticalGridSettingsL5R5E.#setting.world.ranges[index],
|
||||
clientValue: TacticalGridSettingsL5R5E.#setting.client.ranges[index]
|
||||
}));
|
||||
|
||||
return {
|
||||
isGm: game.user.isGM,
|
||||
tactical_grid_enabled: {
|
||||
field: this.constructor.worldSchema.fields.enabled,
|
||||
value: TacticalGridSettingsL5R5E.#setting.world.enabled,
|
||||
},
|
||||
rangeBands,
|
||||
buttons: [
|
||||
{ type: "reset", label: "l5r5e.tactical_grid.settings.reset", icon: "fa-solid fa-arrow-rotate-left", action: "reset" },
|
||||
{ type: "submit", label: "l5r5e.tactical_grid.settings.submit", icon: "fa-solid fa-floppy-disk" }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/** @override ApplicationV2 */
|
||||
_onChangeForm(formConfig, event) {
|
||||
const formData = new foundry.applications.ux.FormDataExtended(this.form, { readonly: true });
|
||||
|
||||
const {
|
||||
cleaned: cleanedWorldSettings,
|
||||
failure: validationFailures
|
||||
} = TacticalGridSettingsL5R5E.#validateAndCleanWorldSettings(formData, event);
|
||||
TacticalGridSettingsL5R5E.#applyValidationState(validationFailures);
|
||||
|
||||
TacticalGridSettingsL5R5E.#setting.world = cleanedWorldSettings
|
||||
TacticalGridSettingsL5R5E.#setting.client = foundry.utils.expandObject(formData.object).l5r5e["tactical-grid-settings-client"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates world schema ensuring range bands are properly ordered and connected.
|
||||
* Note: internal field validation takes precedence, and will result in this validation potentially not running
|
||||
* Checks that:
|
||||
* - Sequential range bands connect properly (range[n].start < range[n+1].start)
|
||||
* @param {*} value - The world settings object to validate
|
||||
* @param {DataFieldValidationOptions} options - Validation options including lastElementChange
|
||||
* @returns {boolean|foundry.data.validation.DataModelValidationFailure} True if valid, otherwise validation failure object
|
||||
*/
|
||||
static #validateWorldRangeConfiguration(value, options) {
|
||||
if(!value.enabled) // don't validate if tactical_grids are disabled
|
||||
return true;
|
||||
|
||||
let previousStart = -1;
|
||||
let previousRangeIndex = null;
|
||||
const failure = new foundry.data.validation.DataModelValidationFailure({ unresolved: true });
|
||||
const changedElementName = options?.element?.name;
|
||||
|
||||
for (const [rangeIndex, range] of Object.entries(value.ranges)) {
|
||||
if (range.start <= previousStart) {
|
||||
let errorKey = TacticalGridSettingsL5R5E.worldSchema.fields.ranges.fields[rangeIndex].fields.start.fieldPath;
|
||||
const previousErrorKey = errorKey.replace(/\.(\d+)\./, `.${previousRangeIndex}.`);
|
||||
|
||||
let isErrorOnPrevious = false;
|
||||
|
||||
// If the previous field was changed, show error there instead
|
||||
if (changedElementName === previousErrorKey) {
|
||||
errorKey = previousErrorKey;
|
||||
isErrorOnPrevious = true;
|
||||
}
|
||||
|
||||
failure.fields[errorKey] = new foundry.data.validation.DataModelValidationFailure({
|
||||
invalidValue: isErrorOnPrevious ? previousStart : range.start,
|
||||
unresolved: true,
|
||||
message: game.i18n.format(
|
||||
isErrorOnPrevious
|
||||
? "l5r5e.tactical_grid.settings.validate.start-too-large"
|
||||
: "l5r5e.tactical_grid.settings.validate.start-too-small",
|
||||
isErrorOnPrevious
|
||||
? { nextRangeIndex: Number(rangeIndex), nextStart: range.start }
|
||||
: { previousRangeIndex: Number(previousRangeIndex), previousStart: previousStart }
|
||||
)
|
||||
});
|
||||
}
|
||||
previousStart = range.start;
|
||||
previousRangeIndex = rangeIndex;
|
||||
}
|
||||
|
||||
return Object.keys(failure.fields).length > 0 ? failure : true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and cleans the world portion of the tactical grid settings.
|
||||
*
|
||||
* Expands raw form data, validates it against the schema, updates form input
|
||||
* error states and tooltips, and returns a cleaned object ready to save.
|
||||
*
|
||||
* @param {foundry.applications.ux.FormDataExtended} formData - The submitted form data.
|
||||
* @param {Event} [event] - Optional event for determining which field changed.
|
||||
* @returns {WorldSettings} A cleaned and validated copy of the world settings.
|
||||
* @private
|
||||
*/
|
||||
static #validateAndCleanWorldSettings(formData, event) {
|
||||
const expanded = foundry.utils.expandObject(formData.object).l5r5e["tactical-grid-settings-world"];
|
||||
const validate = TacticalGridSettingsL5R5E.#schema.world.validate(expanded, { element: event.target});
|
||||
|
||||
// validation from Number etc. itself has the error key just as "ranges.0.start"
|
||||
// so fixing that here so that we can directly reference they html elements
|
||||
const prefix = "l5r5e.tactical-grid-settings-world.";
|
||||
const failures = Object.fromEntries(
|
||||
Object.entries(validate?.asError()?.getAllFailures() ?? {}).map(([key, value]) => [
|
||||
key.startsWith(prefix) ? key : `${prefix}${key}`,
|
||||
value
|
||||
])
|
||||
);
|
||||
|
||||
// Return cleaned schema so that we have something that is somewhat correct we can save
|
||||
return {
|
||||
cleaned: TacticalGridSettingsL5R5E.#schema.world.clean(expanded),
|
||||
failure: failures
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a validation message to a form element.
|
||||
*
|
||||
* @param {HTMLElement} element - The element to apply validation to
|
||||
* @param {string|null} message - The validation message, or null to clear
|
||||
* @private
|
||||
*/
|
||||
static #applyValidationMessage(element, message) {
|
||||
if (message) {
|
||||
element.setCustomValidity(message);
|
||||
element.dataset.tooltip = message;
|
||||
element.ariaLabel = game.i18n.localize(element.dataset.tooltip);
|
||||
game.tooltip.activate(element, {
|
||||
direction: foundry.CONFIG.ux.TooltipManager.TOOLTIP_DIRECTIONS.RIGHT,
|
||||
locked: true
|
||||
});
|
||||
}
|
||||
else {
|
||||
element?.setCustomValidity("");
|
||||
delete element?.dataset?.tooltip
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Applies validation state to all range band start fields.
|
||||
*
|
||||
* @param {Object} failures - Validation failures keyed by field name
|
||||
* @private
|
||||
*/
|
||||
static #applyValidationState(failures) {
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const name = `l5r5e.tactical-grid-settings-world.ranges.${i}.start`;
|
||||
this.#applyValidationMessage(
|
||||
document.getElementsByName(name)[0],
|
||||
failures?.[name]?.message || null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles form submission.
|
||||
*
|
||||
* @param {Event} event - The submission event
|
||||
* @param {HTMLFormElement} form - The form element
|
||||
* @param {foundry.applications.ux.FormDataExtended} formData - The submitted form data
|
||||
* @returns {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
static async #onSubmit(event, form, formData) {
|
||||
const {
|
||||
cleaned: cleanedWorldSettings,
|
||||
failure: validationFailures
|
||||
} = TacticalGridSettingsL5R5E.#validateAndCleanWorldSettings(formData, event);
|
||||
TacticalGridSettingsL5R5E.#applyValidationState(validationFailures);
|
||||
|
||||
TacticalGridSettingsL5R5E.#setting.world = cleanedWorldSettings;
|
||||
TacticalGridSettingsL5R5E.#setting.client = foundry.utils.expandObject(formData.object).l5r5e["tactical-grid-settings-client"];
|
||||
|
||||
const promises = [];
|
||||
promises.push(game.settings.set(CONFIG.l5r5e.namespace, "tactical-grid-settings-world", TacticalGridSettingsL5R5E.#setting.world));
|
||||
promises.push(game.settings.set(CONFIG.l5r5e.namespace, "tactical-grid-settings-client", TacticalGridSettingsL5R5E.#setting.client));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles reset action to restore default settings.
|
||||
*
|
||||
* @param {Event} event - The reset event
|
||||
* @returns {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
static async #onReset(event) {
|
||||
const client = TacticalGridSettingsL5R5E.clientSchema.clean();
|
||||
const world = TacticalGridSettingsL5R5E.worldSchema.clean();
|
||||
TacticalGridSettingsL5R5E.#setting = foundry.utils.deepClone({client: client, world: world});
|
||||
|
||||
await this.render({ force: false });
|
||||
}
|
||||
}
|
||||
|
||||
81
system/scripts/tatical-grid-rulers.js
Normal file
81
system/scripts/tatical-grid-rulers.js
Normal file
@@ -0,0 +1,81 @@
|
||||
function getRangeband(gridSettings, distance) {
|
||||
const entries = Object.entries(gridSettings.ranges);
|
||||
for (let i = entries.length - 1; i >= 0; i--) {
|
||||
const [range, { start }] = entries[i];
|
||||
if (distance >= start) {
|
||||
return Number(range);
|
||||
}
|
||||
}
|
||||
return NaN;
|
||||
}
|
||||
|
||||
export class RulerL5r5e extends foundry.canvas.interaction.Ruler {
|
||||
|
||||
static WAYPOINT_LABEL_TEMPLATE = "systems/l5r5e/templates/" + "hud/tactical-grid-ruler.html"
|
||||
|
||||
/** @override */
|
||||
_getWaypointLabelContext(waypoint, state) {
|
||||
const context = super._getWaypointLabelContext(waypoint, state);
|
||||
if (!context)
|
||||
return;
|
||||
|
||||
const gridSettings = game.settings.get(CONFIG.l5r5e.namespace, "tactical-grid-settings-world");
|
||||
if(gridSettings.enabled) {
|
||||
const diagonalCost = game.canvas.grid.distance * waypoint.measurement.diagonals;
|
||||
context.distance.total = waypoint.measurement.distance.toNearest(0.1) + diagonalCost; //Diagonals count twice
|
||||
context.additional = {
|
||||
label: game.i18n.format("l5r5e.tactical_grid.range_abbreviation", {range: getRangeband(gridSettings, waypoint.measurement.distance)})
|
||||
};
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_getSegmentStyle(waypoint) {
|
||||
const context = super._getSegmentStyle(waypoint);
|
||||
const client = game.settings.get(CONFIG.l5r5e.namespace, "tactical-grid-settings-client");
|
||||
const gridSettings = game.settings.get(CONFIG.l5r5e.namespace, "tactical-grid-settings-world");
|
||||
if(gridSettings.enabled) {
|
||||
const rangeband = getRangeband(gridSettings, waypoint.measurement.distance);
|
||||
context.color = client.ranges[rangeband].color;
|
||||
}
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
export class TokenRulerL5r5e extends foundry.canvas.placeables.tokens.TokenRuler {
|
||||
static WAYPOINT_LABEL_TEMPLATE = "systems/l5r5e/templates/" + "hud/tactical-grid-ruler.html"
|
||||
|
||||
/** @override */
|
||||
_getWaypointLabelContext(waypoint, state) {
|
||||
const context = super._getWaypointLabelContext(waypoint, state);
|
||||
if (!context)
|
||||
return;
|
||||
|
||||
if (!this.token.actor)
|
||||
return context;
|
||||
|
||||
const gridSettings = game.settings.get(CONFIG.l5r5e.namespace, "tactical-grid-settings-world");
|
||||
if(gridSettings.enabled) {
|
||||
const diagonalCost = game.canvas.grid.distance * waypoint.measurement.diagonals;
|
||||
context.cost.total = waypoint.measurement.cost.toNearest(0.1) + diagonalCost; //Diagonals count twice
|
||||
context.additional = {
|
||||
label: game.i18n.format("l5r5e.tactical_grid.range_abbreviation", {range: getRangeband(gridSettings, waypoint.measurement.distance)})
|
||||
};
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_getGridHighlightStyle(waypoint, offset) {
|
||||
const context = super._getGridHighlightStyle(waypoint, offset);
|
||||
const client = game.settings.get(CONFIG.l5r5e.namespace, "tactical-grid-settings-client");
|
||||
const gridSettings = game.settings.get(CONFIG.l5r5e.namespace, "tactical-grid-settings-world");
|
||||
if(gridSettings.enabled) {
|
||||
const rangeband = getRangeband(gridSettings, waypoint.measurement.distance);
|
||||
context.color = client.ranges[rangeband].color;
|
||||
context.alpha = client.ranges[rangeband].alpha;
|
||||
}
|
||||
return context;
|
||||
}
|
||||
}
|
||||
@@ -20,5 +20,6 @@
|
||||
@import "../scss/skills";
|
||||
@import "../scss/items";
|
||||
@import "../scss/twenty-questions";
|
||||
@import "../scss/tactical-grid";
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -10,8 +10,6 @@
|
||||
|
||||
.readiness {
|
||||
flex: 0 0 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
@@ -21,15 +19,20 @@
|
||||
|
||||
li {
|
||||
flex: 25%;
|
||||
display: inline-grid;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
.attributes-buttons {
|
||||
position: relative;
|
||||
line-height: 13px;
|
||||
top: 0.3rem;
|
||||
right: 1.2rem;
|
||||
width: 12px;
|
||||
.increment-control {
|
||||
position: absolute;
|
||||
right: -0.70rem;
|
||||
top: 15%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap:0.1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
strong {
|
||||
@@ -40,14 +43,19 @@
|
||||
}
|
||||
|
||||
label {
|
||||
flex: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
input {
|
||||
background: transparent;
|
||||
border: 0 none;
|
||||
text-align: center;
|
||||
margin: 0.3rem 1.6rem 0 1.5rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
&:after {
|
||||
@@ -55,8 +63,6 @@
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
position: absolute;
|
||||
right: calc(50% - 0.9rem);
|
||||
top: 0.1rem;
|
||||
background: transparent url("../assets/icons/circle.svg") no-repeat 0 0;
|
||||
background-size: contain;
|
||||
opacity: 0.25;
|
||||
@@ -64,9 +70,6 @@
|
||||
}
|
||||
|
||||
&:nth-child(1) {
|
||||
input {
|
||||
margin: 0.3rem 1rem 0 1.5rem;
|
||||
}
|
||||
&:after {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
@@ -79,9 +82,6 @@
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
input {
|
||||
margin: 0.3rem 1rem 0 1.5rem;
|
||||
}
|
||||
&:after {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
@@ -53,6 +53,9 @@ $l5r5e-chat-color-roll: rgba(225, 215, 200, 0.75);
|
||||
$l5r5e-chat-color-blind: transparent;
|
||||
$l5r5e-chat-color-whisper: rgba(225, 200, 225, 0.75);
|
||||
|
||||
// Misc
|
||||
$l5r5e-selection-circle-color: #8a1a00;
|
||||
|
||||
// -- Rings
|
||||
|
||||
// Earth
|
||||
@@ -65,6 +68,7 @@ $l5r5e-water: rgb(95, 145, 155);
|
||||
$l5r5e-fire: rgb(155, 115, 80);
|
||||
// Void
|
||||
$l5r5e-void: rgb(75, 70, 65);
|
||||
$l5r5e-void-light: rgba(207,207,207,.8);
|
||||
|
||||
// -- Clans
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
position: relative;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@@ -20,6 +23,7 @@
|
||||
color: white;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&:last-of-type,
|
||||
&:nth-child(6),
|
||||
&:nth-child(12),
|
||||
@@ -28,10 +32,12 @@
|
||||
padding: 0 0.175rem 0 0.15rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.rerolled {
|
||||
> img {
|
||||
border-bottom: 0 none;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: "\f2f9";
|
||||
background: orangered;
|
||||
@@ -42,6 +48,7 @@
|
||||
> img {
|
||||
border-bottom: 0 none;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: "\f337";
|
||||
background: fuchsia;
|
||||
@@ -50,9 +57,9 @@
|
||||
|
||||
> img {
|
||||
border: 1px solid transparent;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
.chat-dice {
|
||||
}
|
||||
@@ -72,6 +79,7 @@
|
||||
margin: 0.25rem;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
&-element {
|
||||
flex-wrap: wrap;
|
||||
flex-grow: 1;
|
||||
@@ -79,11 +87,13 @@
|
||||
&-skill {
|
||||
flex-grow: 3;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
flex-grow: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dice-formula,
|
||||
.dice-total {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
@@ -91,15 +101,19 @@
|
||||
text-align: center;
|
||||
margin: 0.5rem 0;
|
||||
padding: 0.25rem 0.5rem 0.25rem 0.25rem;
|
||||
|
||||
&-rnk {
|
||||
line-height: 2rem;
|
||||
|
||||
i {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
justify-self: center;
|
||||
|
||||
&.chat-dice-rnk {
|
||||
cursor: url("../assets/cursors/pointer.webp"), pointer;
|
||||
color: $white;
|
||||
@@ -113,6 +127,7 @@
|
||||
border-image-width: 0.5rem;
|
||||
border-image-outset: 0px;
|
||||
margin: 0.5rem 0 0;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(
|
||||
$l5r5e-linear-gradient-first-dark,
|
||||
@@ -121,6 +136,7 @@
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&.chat-dice-rnk-ended {
|
||||
background: linear-gradient(
|
||||
$l5r5e-linear-gradient-second,
|
||||
@@ -129,6 +145,7 @@
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.dice-result-rnk {
|
||||
background: rgba(0, 0, 255, 0.1);
|
||||
border: 1px solid rgba(55, 55, 155, 0.75);
|
||||
@@ -137,25 +154,30 @@
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 0 $black;
|
||||
|
||||
&.success {
|
||||
background: rgba(0, 255, 0, 0.1);
|
||||
border-color: rgba(55, 155, 55, 0.75);
|
||||
color: rgba(55, 155, 55, 0.75);
|
||||
|
||||
i.i_success {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.unknown {
|
||||
background: rgba(121, 121, 121, 0.1);
|
||||
border-color: rgba(124, 124, 124, 0.75);
|
||||
color: rgba(91, 91, 91, 0.75);
|
||||
}
|
||||
|
||||
&.fail {
|
||||
background: rgba(255, 0, 0, 0.1);
|
||||
border-color: rgba(155, 55, 55, 0.75);
|
||||
color: rgba(155, 55, 55, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
.target,
|
||||
.item-infos {
|
||||
display: flex;
|
||||
@@ -166,20 +188,25 @@
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: solid 1px rgba(100, 0, 0, 0.75);
|
||||
border-radius: 3px;
|
||||
|
||||
.profile {
|
||||
flex: 1;
|
||||
margin: 0.25rem 0.25rem 0 0;
|
||||
position: relative;
|
||||
|
||||
.profile-img {
|
||||
position: relative;
|
||||
border: none;
|
||||
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.66));
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
flex: 6;
|
||||
font-family: "BrushtipTexe", sans-serif;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.content-link {
|
||||
background: unset;
|
||||
border: unset;
|
||||
@@ -188,8 +215,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-infos {
|
||||
border: solid 1px rgba(0, 78, 100, 0.75);
|
||||
|
||||
i {
|
||||
font-size: var(--font-size-12);
|
||||
}
|
||||
@@ -199,3 +228,4 @@
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,55 @@
|
||||
cursor: url("../assets/cursors/pointer.webp"), pointer;
|
||||
}
|
||||
|
||||
// define a mixin
|
||||
@mixin roll-effects-base {
|
||||
clear: both;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 2px 4px;
|
||||
|
||||
.effect-container {
|
||||
border: 1px solid #5a6e5a;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
padding: 3px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.effect-delete {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
text-align: end;
|
||||
cursor: url("../assets/cursors/pointer.webp"), pointer;
|
||||
}
|
||||
|
||||
.effect-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.effect-name {
|
||||
vertical-align: top;
|
||||
white-space: nowrap;
|
||||
color: $white;
|
||||
margin-left: 4px;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
// Dice Picker
|
||||
.dice-picker-dialog {
|
||||
min-width: 35rem;
|
||||
.effects {
|
||||
@include roll-effects-base();
|
||||
}
|
||||
|
||||
width: 35rem;
|
||||
min-height: auto;
|
||||
// Utility
|
||||
* {
|
||||
@@ -167,6 +213,7 @@
|
||||
td {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,6 +253,12 @@
|
||||
|
||||
.profil {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column; // stack profile ul and effects ul vertically
|
||||
|
||||
.effects {
|
||||
@include roll-effects-base();
|
||||
}
|
||||
}
|
||||
|
||||
.dropbox {
|
||||
@@ -258,6 +311,7 @@
|
||||
.dice-ct {
|
||||
position: relative;
|
||||
padding: 0.25rem;
|
||||
width: 48px;
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.application {
|
||||
color: var(--color-text-dark-primary);
|
||||
color: var(--color-text-primary);
|
||||
|
||||
.scrollable {
|
||||
--scroll-margin: 0;
|
||||
@@ -48,10 +48,70 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Sidebar here. Starting with custom icons
|
||||
$l5r5e-custom-icons: (
|
||||
chatIcon: "../assets/ui/sidebar/chat.svg",
|
||||
combatIcon: "../assets/ui/sidebar/combat-tracker.svg",
|
||||
sceneIcon: "../assets/ui/sidebar/scenes.svg",
|
||||
actorIcon: "../assets/ui/sidebar/actors.svg",
|
||||
itemIcon: "../assets/ui/sidebar/object.svg",
|
||||
journalIcon: "../assets/ui/sidebar/journal.svg",
|
||||
rolltableIcon: "../assets/ui/sidebar/rolltable.svg",
|
||||
playlistIcon: "../assets/ui/sidebar/playlist.svg",
|
||||
compendiumIcon: "../assets/ui/sidebar/compendium.svg",
|
||||
settingsIcon: "../assets/ui/sidebar/settings.svg"
|
||||
);
|
||||
|
||||
$selectors: (
|
||||
"#sidebar-tabs button.l5r5e",
|
||||
"#sidebar-content .create-button.l5r5e",
|
||||
"#sidebar-content i.l5r5e",
|
||||
"#context-menu i.l5r5e"
|
||||
);
|
||||
|
||||
@each $selector in $selectors {
|
||||
#{$selector} {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@if str-index($selector, "i.") {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
@if str-index($selector, "create-button") {
|
||||
filter: drop-shadow(0 0 3px var(--color-dark-1));
|
||||
}
|
||||
|
||||
// Apply masks for each icon
|
||||
@each $name, $url in $l5r5e-custom-icons {
|
||||
&.#{$name}::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 85%;
|
||||
height: 85%;
|
||||
background-color: currentColor;
|
||||
mask: url($url) no-repeat center / contain;
|
||||
-webkit-mask: url($url) no-repeat center / contain;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Generic plus icon badge
|
||||
&.icon-plus::after {
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#sidebar-tabs > menu {
|
||||
gap: 4px; // halve the distance between menu icons
|
||||
}
|
||||
|
||||
|
||||
#sidebar-content.expanded {
|
||||
background: url("../assets/ui/bgSidebar.webp") no-repeat;
|
||||
border-image: url("../assets/ui/macro-button.webp") 10 repeat;
|
||||
|
||||
@@ -145,6 +145,11 @@ input[type="time"]:focus {
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
// Remove icon on link (bc specificity)
|
||||
#sidebar-content .content-link i.l5r5e {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
box-shadow: 0 0 10px $red;
|
||||
}
|
||||
@@ -182,13 +187,13 @@ fieldset {
|
||||
}
|
||||
.editor {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Editors
|
||||
.editor,
|
||||
.editor-container {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -227,6 +232,7 @@ sup {
|
||||
}
|
||||
|
||||
.editor-container,
|
||||
.editor-content,
|
||||
.item-description {
|
||||
ul {
|
||||
margin: 0.5rem 0;
|
||||
|
||||
@@ -326,7 +326,7 @@
|
||||
margin: 0.25rem 0;
|
||||
flex-direction: row-reverse;
|
||||
strong {
|
||||
flex: 0 0 calc(100% - 3.5rem);
|
||||
flex: 0 0 calc(100% - 4.5rem);
|
||||
}
|
||||
input {
|
||||
flex: 0 0 3rem;
|
||||
@@ -444,7 +444,7 @@
|
||||
width: 3.5rem;
|
||||
}
|
||||
}
|
||||
.attributes-buttons {
|
||||
.increment-control {
|
||||
line-height: 13px;
|
||||
position: relative;
|
||||
top: 0.2rem;
|
||||
@@ -751,12 +751,22 @@
|
||||
flex: calc(100% / 3);
|
||||
padding: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
align-items: center;
|
||||
input {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
.xp-buttons,
|
||||
.money-buttons {
|
||||
line-height: 13px;
|
||||
padding-left: 0.3em;
|
||||
}
|
||||
.increment-control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-left: 0.2rem;
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
32
system/styles/scss/tactical-grid.scss
Normal file
32
system/styles/scss/tactical-grid.scss
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
// Set the label for in-world measurement to be the same as normal waypoint-label
|
||||
@at-root #measurement .waypoint-label-additional {
|
||||
color: var(--color-text-emphatic);
|
||||
font-size: var(--font-size-24);
|
||||
}
|
||||
|
||||
@at-root #tactical-grid-settings {
|
||||
input[type="number"]:invalid {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
input[type="number"]:read-only {
|
||||
border: none;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
pointer-events: none; /* not clickable or focusable */
|
||||
user-select: none; /* text cannot be selected */
|
||||
-webkit-user-select: none; /* Safari/Chrome */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
}
|
||||
|
||||
.range_band {
|
||||
display: flex;
|
||||
flex-flow: wrap;
|
||||
fieldset {
|
||||
flex: 25%;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,12 +7,12 @@
|
||||
"changelog": "https://gitlab.com/teaml5r/l5r5e/-/blob/master/CHANGELOG.md",
|
||||
"license": "https://gitlab.com/teaml5r/l5r5e/-/blob/master/LICENSE.md",
|
||||
"manifest": "https://gitlab.com/teaml5r/l5r5e/-/raw/master/system/system.json",
|
||||
"download": "https://gitlab.com/teaml5r/l5r5e/-/jobs/artifacts/v1.13.2/raw/l5r5e.zip?job=build",
|
||||
"version": "1.13.2",
|
||||
"download": "https://gitlab.com/teaml5r/l5r5e/-/jobs/artifacts/v1.14.0/raw/l5r5e.zip?job=build",
|
||||
"version": "1.14.0",
|
||||
"compatibility": {
|
||||
"minimum": "13",
|
||||
"verified": "13",
|
||||
"maximum": "13"
|
||||
"maximum": "14"
|
||||
},
|
||||
"socket": true,
|
||||
"authors": [
|
||||
@@ -29,6 +29,10 @@
|
||||
"name": "Carter",
|
||||
"discord": "Carter#2703",
|
||||
"url": "https://fr.tipeee.com/carter-foundryvtt"
|
||||
},
|
||||
{
|
||||
"name": "Litasa",
|
||||
"discord": "Litasa#3139"
|
||||
}
|
||||
],
|
||||
"background": "systems/l5r5e/assets/l5r-header.webp",
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<ul>
|
||||
{{#each data.splitTechniquesList as |list techName|}}
|
||||
<li>
|
||||
<strong>{{localize (localize 'l5r5e.techniques.{techName}' techName=techName) }}</strong>
|
||||
<strong>{{localizeEmbedded 'l5r5e.techniques.{techName}' techName=techName}}</strong>
|
||||
<ul>
|
||||
{{#each list as |technique|}}
|
||||
<li>
|
||||
@@ -130,7 +130,7 @@
|
||||
<li>{{localize 'l5r5e.money.title'}} : {{data.system.money.koku}} {{localize 'l5r5e.money.koku'}} / {{data.system.money.bu}} {{localize 'l5r5e.money.bu'}} / {{data.system.money.zeni}} {{localize 'l5r5e.money.zeni'}}</li>
|
||||
{{#each data.splitItemsList as |cat type|}}
|
||||
<li>
|
||||
<strong>{{localize (localize 'l5r5e.{type}s.title' type=type)}} ({{cat.length}})</strong>
|
||||
<strong>{{localizeEmbedded 'l5r5e.{type}s.title' type=type}} ({{cat.length}})</strong>
|
||||
<ul>
|
||||
{{#each cat as |item|}}
|
||||
<li>{{> 'systems/l5r5e/templates/items/item/item-text.html' data=item editable=../../options.editable}}</li>
|
||||
@@ -209,20 +209,20 @@
|
||||
{{/ifCond}}
|
||||
<h2>{{localize 'l5r5e.twenty_questions.title'}}</h2>
|
||||
<ul>
|
||||
<li>{{localize (localize 'l5r5e.twenty_questions.part2.q4{suffix}' suffix=suffix)}} : {{data.system.twenty_questions.step4.stand_out}}</li>
|
||||
<li>{{localize (localize 'l5r5e.twenty_questions.part3.q7{suffix}' suffix=suffix)}} : {{data.system.twenty_questions.step7.clan_relations}}</li>
|
||||
<li>{{localize (localize 'l5r5e.twenty_questions.part3.q8{suffix}' suffix=suffix)}} : {{data.system.twenty_questions.step8.bushido}}</li>
|
||||
<li>{{localize (localize 'l5r5e.twenty_questions.part4.q9{suffix}' suffix=suffix)}} : {{data.system.twenty_questions.step9.success}}</li>
|
||||
<li>{{localize (localize 'l5r5e.twenty_questions.part4.q10{suffix}' suffix=suffix)}} : {{data.system.twenty_questions.step10.difficulty}}</li>
|
||||
<li>{{localize (localize 'l5r5e.twenty_questions.part4.q11{suffix}' suffix=suffix)}} : {{data.system.twenty_questions.step11.calms}}</li>
|
||||
<li>{{localize (localize 'l5r5e.twenty_questions.part4.q12{suffix}' suffix=suffix)}} : {{data.system.twenty_questions.step12.worries}}</li>
|
||||
<li>{{localize (localize 'l5r5e.twenty_questions.part4.q13{suffix}' suffix=suffix)}} : {{data.system.twenty_questions.step13.most_learn}}</li>
|
||||
<li>{{localize (localize 'l5r5e.twenty_questions.part5.q14{suffix}' suffix=suffix)}} : {{data.system.twenty_questions.step14.first_sight}}</li>
|
||||
<li>{{localize (localize 'l5r5e.twenty_questions.part5.q15{suffix}' suffix=suffix)}} : {{data.system.twenty_questions.step15.stress}}</li>
|
||||
<li>{{localize (localize 'l5r5e.twenty_questions.part5.q16{suffix}' suffix=suffix)}} : {{data.system.twenty_questions.step16.relations}}</li>
|
||||
<li>{{localize (localize 'l5r5e.twenty_questions.part6.q17{suffix}' suffix=suffix)}} : {{data.system.twenty_questions.step17.parents_pov}}</li>
|
||||
<li>{{localize (localize 'l5r5e.twenty_questions.part6.q18{suffix}' suffix=suffix)}} : {{data.system.twenty_questions.step18.heritage_name}}</li>
|
||||
<li>{{localize (localize 'l5r5e.twenty_questions.part7.q20{suffix}' suffix=suffix)}} : {{data.system.twenty_questions.step20.death}}</li>
|
||||
<li>{{localizeEmbedded 'l5r5e.twenty_questions.part2.q4{suffix}' suffix}} : {{data.system.twenty_questions.step4.stand_out}}</li>
|
||||
<li>{{localizeEmbedded 'l5r5e.twenty_questions.part3.q7{suffix}' suffix}} : {{data.system.twenty_questions.step7.clan_relations}}</li>
|
||||
<li>{{localizeEmbedded 'l5r5e.twenty_questions.part3.q8{suffix}' suffix}} : {{data.system.twenty_questions.step8.bushido}}</li>
|
||||
<li>{{localizeEmbedded 'l5r5e.twenty_questions.part4.q9{suffix}' suffix}} : {{data.system.twenty_questions.step9.success}}</li>
|
||||
<li>{{localizeEmbedded 'l5r5e.twenty_questions.part4.q10{suffix}' suffix}} : {{data.system.twenty_questions.step10.difficulty}}</li>
|
||||
<li>{{localizeEmbedded 'l5r5e.twenty_questions.part4.q11{suffix}' suffix}} : {{data.system.twenty_questions.step11.calms}}</li>
|
||||
<li>{{localizeEmbedded 'l5r5e.twenty_questions.part4.q12{suffix}' suffix}} : {{data.system.twenty_questions.step12.worries}}</li>
|
||||
<li>{{localizeEmbedded 'l5r5e.twenty_questions.part4.q13{suffix}' suffix}} : {{data.system.twenty_questions.step13.most_learn}}</li>
|
||||
<li>{{localizeEmbedded 'l5r5e.twenty_questions.part5.q14{suffix}' suffix}} : {{data.system.twenty_questions.step14.first_sight}}</li>
|
||||
<li>{{localizeEmbedded 'l5r5e.twenty_questions.part5.q15{suffix}' suffix}} : {{data.system.twenty_questions.step15.stress}}</li>
|
||||
<li>{{localizeEmbedded 'l5r5e.twenty_questions.part5.q16{suffix}' suffix}} : {{data.system.twenty_questions.step16.relations}}</li>
|
||||
<li>{{localizeEmbedded 'l5r5e.twenty_questions.part6.q17{suffix}' suffix}} : {{data.system.twenty_questions.step17.parents_pov}}</li>
|
||||
<li>{{localizeEmbedded 'l5r5e.twenty_questions.part6.q18{suffix}' suffix}} : {{data.system.twenty_questions.step18.heritage_name}}</li>
|
||||
<li>{{localizeEmbedded 'l5r5e.twenty_questions.part7.q20{suffix}' suffix}} : {{data.system.twenty_questions.step20.death}}</li>
|
||||
</ul>
|
||||
|
||||
</section>
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
<div class="readiness">
|
||||
<ul>
|
||||
<li>
|
||||
<label class="attribute-label-casualties">
|
||||
<label class="attribute-label casualties">
|
||||
<input name="system.battle_readiness.casualties_strength.value" type="number" value="{{data.system.battle_readiness.casualties_strength.value}}" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
|
||||
<span class="attributes-buttons">
|
||||
<span class="increment-control casualties">
|
||||
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="casualties" data-value="1"></i>
|
||||
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="casualties" data-value="-1"></i>
|
||||
</span>
|
||||
@@ -23,15 +23,15 @@
|
||||
<strong>{{localize 'l5r5e.army.battle_readiness.casualties'}}</strong>
|
||||
</li>
|
||||
<li>
|
||||
<label class="attribute-label-strength">
|
||||
<label class="attribute-label strength">
|
||||
<input name="system.battle_readiness.casualties_strength.max" type="number" value="{{data.system.battle_readiness.casualties_strength.max}}" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
|
||||
</label>
|
||||
<strong>{{localize 'l5r5e.army.battle_readiness.strength'}}</strong>
|
||||
</li>
|
||||
<li>
|
||||
<label class="attribute-label-panic">
|
||||
<label class="attribute-label panic">
|
||||
<input name="system.battle_readiness.panic_discipline.value" type="number" value="{{data.system.battle_readiness.panic_discipline.value}}" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
|
||||
<span class="attributes-buttons">
|
||||
<span class="increment-control panic">
|
||||
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="panic" data-value="1"></i>
|
||||
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="panic" data-value="-1"></i>
|
||||
</span>
|
||||
@@ -39,7 +39,7 @@
|
||||
<strong>{{localize 'l5r5e.army.battle_readiness.panic'}}</strong>
|
||||
</li>
|
||||
<li>
|
||||
<label class="attribute-label-discipline">
|
||||
<label class="attribute-label discipline">
|
||||
<input name="system.battle_readiness.panic_discipline.max" type="number" value="{{data.system.battle_readiness.panic_discipline.max}}" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
|
||||
</label>
|
||||
<strong>{{localize 'l5r5e.army.battle_readiness.discipline'}}</strong>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<label class="attribute-label">
|
||||
<strong>{{localize 'l5r5e.attributes.fatigue'}}</strong>
|
||||
<input class="centered-input select-on-focus" type="number" name="system.fatigue.value" value="{{data.system.fatigue.value}}" data-dtype="Number" min="0" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
|
||||
<span class="attributes-buttons">
|
||||
<span class="increment-control fatigue">
|
||||
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="fatigue" data-value="1"></i>
|
||||
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="fatigue" data-value="-1"></i>
|
||||
</span>
|
||||
@@ -22,7 +22,7 @@
|
||||
<label class="attribute-label">
|
||||
<strong>{{localize 'l5r5e.attributes.strife'}}</strong>
|
||||
<input class="centered-input select-on-focus" type="number" name="system.strife.value" value="{{data.system.strife.value}}" data-dtype="Number" min="0" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
|
||||
<span class="attributes-buttons">
|
||||
<span class="increment-control strife">
|
||||
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="strife" data-value="1"></i>
|
||||
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="strife" data-value="-1"></i>
|
||||
</span>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<legend class="section-header">
|
||||
{{localize 'l5r5e.conflict.initiative.title'}}
|
||||
<a class="encounter prepared-control" data-id="{{data.type}}">
|
||||
<i class="fa fas prepared-icon prepared-icon-{{data.system.prepared}} prepared-{{data.type}}" title="{{localize (localize 'l5r5e.conflict.initiative.prepared_{value}' value=data.system.prepared)}}"></i>
|
||||
<i class="fa fas prepared-icon prepared-icon-{{data.system.prepared}} prepared-{{data.type}}" title="{{localizeEmbedded 'l5r5e.conflict.initiative.prepared_{value}' value=data.system.prepared}}"></i>
|
||||
</a>
|
||||
</legend>
|
||||
<button class="initiative dice-picker" data-initiative="true" data-skill="sentiment">{{localize 'l5r5e.conflict.initiative.intrigue'}}</button>
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
<label class="attribute-label">
|
||||
{{localize 'l5r5e.advancements.total'}}
|
||||
<input class="centered-input select-on-focus" type="number" name="system.xp_total" value="{{data.system.xp_total}}" data-dtype="Number" min="0" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
|
||||
<span class="increment-control xp">
|
||||
<i class="xp-control pointer-choice fa fa-plus-square" data-type="xp" data-value="1"></i>
|
||||
<i class="xp-control pointer-choice fa fa-minus-square" data-type="xp" data-value="-1"></i>
|
||||
</span>
|
||||
</label>
|
||||
<label class="attribute-label">
|
||||
{{localize 'l5r5e.advancements.spent'}}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
<fieldset class="money money-wrapper">
|
||||
<legend class="section-header">{{localize 'l5r5e.money.title'}}</legend>
|
||||
<label>
|
||||
<label class="attribute-label money">
|
||||
{{localize 'l5r5e.money.koku'}}
|
||||
<input name="system.money.koku" type="number" value="{{data.system.money.koku}}" data-dtype="Number" min="0" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
|
||||
<span class="money-buttons">
|
||||
<span class="increment-control money">
|
||||
<i class="money-control pointer-choice fa fa-plus-square" data-type="koku" data-value="1"></i>
|
||||
<i class="money-control pointer-choice fa fa-minus-square" data-type="koku" data-value="-1"></i>
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
<label class="attribute-label money">
|
||||
{{localize 'l5r5e.money.bu'}}
|
||||
<input name="system.money.bu" type="number" value="{{data.system.money.bu}}" data-dtype="Number" min="0" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
|
||||
<span class="money-buttons">
|
||||
<span class="increment-control money">
|
||||
<i class="money-control pointer-choice fa fa-plus-square" data-type="bu" data-value="1"></i>
|
||||
<i class="money-control pointer-choice fa fa-minus-square" data-type="bu" data-value="-1"></i>
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
<label class="attribute-label money">
|
||||
{{localize 'l5r5e.money.zeni'}}
|
||||
<input name="system.money.zeni" type="number" value="{{data.system.money.zeni}}" data-dtype="Number" min="0" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
|
||||
<span class="money-buttons">
|
||||
<span class="increment-control money">
|
||||
<i class="money-control pointer-choice fa fa-plus-square" data-type="zeni" data-value="1"></i>
|
||||
<i class="money-control pointer-choice fa fa-minus-square" data-type="zeni" data-value="-1"></i>
|
||||
</span>
|
||||
|
||||
@@ -2,18 +2,30 @@
|
||||
<li>
|
||||
<label class="attribute-label centered-input">
|
||||
<strong>{{localize 'l5r5e.social.honor'}}</strong>
|
||||
<span class="increment-control honor">
|
||||
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="honor" data-value="1"></i>
|
||||
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="honor" data-value="-1"></i>
|
||||
</span>
|
||||
<input class="centered-input select-on-focus" type="number" name="system.social.honor" value="{{data.system.social.honor}}" data-dtype="Number" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="attribute-label centered-input">
|
||||
<strong>{{localize 'l5r5e.social.glory'}}</strong>
|
||||
<span class="increment-control glory">
|
||||
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="glory" data-value="1"></i>
|
||||
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="glory" data-value="-1"></i>
|
||||
</span>
|
||||
<input class="centered-input select-on-focus" type="number" name="system.social.glory" value="{{data.system.social.glory}}" data-dtype="Number" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="attribute-label centered-input">
|
||||
<strong>{{localize 'l5r5e.social.status'}}</strong>
|
||||
<span class="increment-control status">
|
||||
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="status" data-value="1"></i>
|
||||
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="status" data-value="-1"></i>
|
||||
</span>
|
||||
<input class="centered-input select-on-focus" type="number" name="system.social.status" value="{{data.system.social.status}}" data-dtype="Number" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
|
||||
</label>
|
||||
</li>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<fieldset class="section-header flexrow">
|
||||
<legend class="technique-controls">
|
||||
<span class="technique-controls toggle-on-click" data-toggle="toggle-technique-category-{{technique}}">
|
||||
{{localize (localize 'l5r5e.techniques.{technique}' technique=technique)}}
|
||||
{{localizeEmbedded 'l5r5e.techniques.{technique}' technique=technique}}
|
||||
</span>
|
||||
{{#ifCond ../data.editable_not_soft_locked '&&' (lookup ../data.system.techniques technique)}}
|
||||
<a data-item-type="technique" class="technique-control item-add" data-tech-type="{{technique}}" title="{{localize 'l5r5e.global.add'}}"><i class="fas fa-plus"></i></a>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<label class="attribute-label">
|
||||
<strong>{{localize 'l5r5e.attributes.fatigue'}}</strong>
|
||||
<input class="centered-input select-on-focus" type="number" name="system.fatigue.value" value="{{data.system.fatigue.value}}" data-dtype="Number" min="0" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
|
||||
<span class="attributes-buttons">
|
||||
<span class="increment-control fatigue">
|
||||
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="fatigue" data-value="1"></i>
|
||||
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="fatigue" data-value="-1"></i>
|
||||
</span>
|
||||
@@ -22,7 +22,7 @@
|
||||
<label class="attribute-label">
|
||||
<strong>{{localize 'l5r5e.attributes.strife'}}</strong>
|
||||
<input class="centered-input select-on-focus" type="number" name="system.strife.value" value="{{data.system.strife.value}}" data-dtype="Number" min="0" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
|
||||
<span class="attributes-buttons">
|
||||
<span class="increment-control strife">
|
||||
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="strife" data-value="1"></i>
|
||||
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="strife" data-value="-1"></i>
|
||||
</span>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<legend class="section-header">
|
||||
{{localize 'l5r5e.conflict.initiative.title'}}
|
||||
<a class="encounter prepared-control" data-id="{{data.type}}">
|
||||
<i class="fa fas prepared-icon prepared-icon-{{data.system.prepared}} prepared-{{data.system.type}}" title="{{localize (localize 'l5r5e.conflict.initiative.prepared_{value}' value=data.system.prepared)}}"></i>
|
||||
<i class="fa fas prepared-icon prepared-icon-{{data.system.prepared}} prepared-{{data.system.type}}" title="{{localizeEmbedded 'l5r5e.conflict.initiative.prepared_{value}' value=data.system.prepared}}"></i>
|
||||
</a>
|
||||
</legend>
|
||||
<button class="initiative dice-picker" data-initiative="true" data-skill="sentiment">{{localize 'l5r5e.conflict.initiative.intrigue'}}</button>
|
||||
|
||||
@@ -2,18 +2,30 @@
|
||||
<li>
|
||||
<label class="attribute-label centered-input">
|
||||
<strong>{{localize 'l5r5e.social.honor'}}</strong>
|
||||
<span class="increment-control honor">
|
||||
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="honor" data-value="1"></i>
|
||||
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="honor" data-value="-1"></i>
|
||||
</span>
|
||||
<input class="centered-input select-on-focus" type="number" name="system.social.honor" value="{{data.system.social.honor}}" data-dtype="Number" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="attribute-label centered-input">
|
||||
<strong>{{localize 'l5r5e.social.glory'}}</strong>
|
||||
<span class="increment-control glory">
|
||||
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="glory" data-value="1"></i>
|
||||
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="glory" data-value="-1"></i>
|
||||
</span>
|
||||
<input class="centered-input select-on-focus" type="number" name="system.social.glory" value="{{data.system.social.glory}}" data-dtype="Number" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="attribute-label centered-input">
|
||||
<strong>{{localize 'l5r5e.social.status'}}</strong>
|
||||
<span class="increment-control status">
|
||||
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="status" data-value="1"></i>
|
||||
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="status" data-value="-1"></i>
|
||||
</span>
|
||||
<input class="centered-input select-on-focus" type="number" name="system.social.status" value="{{data.system.social.status}}" data-dtype="Number" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
|
||||
</label>
|
||||
</li>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<fieldset class="section-header flexrow">
|
||||
<legend class="technique-controls">
|
||||
<span class="technique-controls toggle-on-click" data-toggle="toggle-technique-category-{{technique}}">
|
||||
{{localize (localize 'l5r5e.techniques.{technique}' technique=technique)}}
|
||||
{{localizeEmbedded 'l5r5e.techniques.{technique}' technique=technique}}
|
||||
</span>
|
||||
{{#ifCond ../data.editable_not_soft_locked '&&' (lookup ../data.system.techniques technique)}}
|
||||
<a data-item-type="technique" class="technique-control item-add" data-tech-type="{{technique}}" title="{{localize 'l5r5e.global.add'}}"><i class="fas fa-plus"></i></a>
|
||||
|
||||
@@ -27,12 +27,12 @@
|
||||
</label>
|
||||
<br>
|
||||
<br>
|
||||
<i>{{localize (localize 'l5r5e.twenty_questions.part0.section{suffix}' suffix=suffix)}}</i>
|
||||
<i>{{localizeEmbedded 'l5r5e.twenty_questions.part0.section{suffix}' suffix=suffix}}</i>
|
||||
<button class="next" name="next" type="button">{{localize 'l5r5e.twenty_questions.bt_next'}} <i class='fas fa-arrow-right'></i></button>
|
||||
</article>
|
||||
<article class="tab parts part1" data-group="primary" data-tab="part1">
|
||||
<h2>{{localize (localize 'l5r5e.twenty_questions.part1.title{suffix}' suffix=suffix)}}</h2>
|
||||
<h3>{{localize (localize 'l5r5e.twenty_questions.part1.q1{suffix}' suffix=suffix)}}</h3>
|
||||
<h2>{{localizeEmbedded 'l5r5e.twenty_questions.part1.title{suffix}' suffix=suffix}}</h2>
|
||||
<h3>{{localizeEmbedded 'l5r5e.twenty_questions.part1.q1{suffix}' suffix=suffix}}</h3>
|
||||
<div>
|
||||
<input type="text" name="step1.clan" value="{{data.step1.clan}}">
|
||||
</div>
|
||||
@@ -73,7 +73,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h3>{{localize (localize 'l5r5e.twenty_questions.part1.q2{suffix}' suffix=suffix)}}</h3>
|
||||
<h3>{{localizeEmbedded 'l5r5e.twenty_questions.part1.q2{suffix}' suffix=suffix}}</h3>
|
||||
<div>
|
||||
<input type="text" name="step2.family" value="{{data.step2.family}}">
|
||||
</div>
|
||||
@@ -122,7 +122,7 @@
|
||||
</article>
|
||||
<article class="tab parts part2" data-group="primary" data-tab="part2">
|
||||
<h2>{{localize 'l5r5e.twenty_questions.part2.title'}}</h2>
|
||||
<h3>{{localize (localize 'l5r5e.twenty_questions.part2.q3{suffix}' suffix=suffix)}}</h3>
|
||||
<h3>{{localizeEmbedded 'l5r5e.twenty_questions.part2.q3{suffix}' suffix=suffix}}</h3>
|
||||
<label class="full">
|
||||
{{localize 'l5r5e.twenty_questions.part2.school'}}
|
||||
<input type="text" name="step3.school" value="{{data.step3.school}}">
|
||||
@@ -214,7 +214,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h3>{{localize (localize 'l5r5e.twenty_questions.part2.q4{suffix}' suffix=suffix)}}</h3>
|
||||
<h3>{{localizeEmbedded 'l5r5e.twenty_questions.part2.q4{suffix}' suffix=suffix}}</h3>
|
||||
<textarea name="step4.stand_out">{{data.step4.stand_out}}</textarea>
|
||||
<label>
|
||||
{{localize 'l5r5e.twenty_questions.increase_ring1'}}
|
||||
@@ -226,8 +226,8 @@
|
||||
<button class="next" name="next" type="button">{{localize 'l5r5e.twenty_questions.bt_next'}} <i class='fas fa-arrow-right'></i></button>
|
||||
</article>
|
||||
<article class="tab parts part3" data-group="primary" data-tab="part3">
|
||||
<h2>{{localize (localize 'l5r5e.twenty_questions.part3.title{suffix}' suffix=suffix)}}</h2>
|
||||
<h3>{{localize (localize 'l5r5e.twenty_questions.part3.q5{suffix}' suffix=suffix)}}</h3>
|
||||
<h2>{{localizeEmbedded 'l5r5e.twenty_questions.part3.title{suffix}' suffix=suffix}}</h2>
|
||||
<h3>{{localizeEmbedded 'l5r5e.twenty_questions.part3.q5{suffix}' suffix=suffix}}</h3>
|
||||
<label>
|
||||
{{#ifCond data.template '==' 'core'}}
|
||||
{{localize 'l5r5e.twenty_questions.part3.choose_giri'}}
|
||||
@@ -236,12 +236,12 @@
|
||||
{{/ifCond}}
|
||||
<textarea name="step5.social_giri">{{data.step5.social_giri}}</textarea>
|
||||
</label>
|
||||
<h3>{{localize (localize 'l5r5e.twenty_questions.part3.q6{suffix}' suffix=suffix)}}</h3>
|
||||
<h3>{{localizeEmbedded 'l5r5e.twenty_questions.part3.q6{suffix}' suffix=suffix}}</h3>
|
||||
<label>
|
||||
{{localize 'l5r5e.twenty_questions.part3.choose_ninjo'}}
|
||||
<textarea name="step6.social_ninjo">{{data.step6.social_ninjo}}</textarea>
|
||||
</label>
|
||||
<h3>{{localize (localize 'l5r5e.twenty_questions.part3.q7{suffix}' suffix=suffix)}}</h3>
|
||||
<h3>{{localizeEmbedded 'l5r5e.twenty_questions.part3.q7{suffix}' suffix=suffix}}</h3>
|
||||
<textarea name="step7.clan_relations">{{data.step7.clan_relations}}</textarea>
|
||||
<table>
|
||||
<tr>
|
||||
@@ -265,7 +265,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h3>{{localize (localize 'l5r5e.twenty_questions.part3.q8{suffix}' suffix=suffix)}}</h3>
|
||||
<h3>{{localizeEmbedded 'l5r5e.twenty_questions.part3.q8{suffix}' suffix=suffix}}</h3>
|
||||
<textarea name="step8.bushido">{{data.step8.bushido}}</textarea>
|
||||
<table>
|
||||
<tr>
|
||||
@@ -315,31 +315,31 @@
|
||||
</article>
|
||||
<article class="tab parts part4" data-group="primary" data-tab="part4">
|
||||
<h2>{{localize 'l5r5e.twenty_questions.part4.title'}}</h2>
|
||||
<h3>{{localize (localize 'l5r5e.twenty_questions.part4.q9{suffix}' suffix=suffix)}}</h3>
|
||||
<h3>{{localizeEmbedded 'l5r5e.twenty_questions.part4.q9{suffix}' suffix=suffix}}</h3>
|
||||
<textarea name="step9.success">{{data.step9.success}}</textarea>
|
||||
<label>
|
||||
{{localize 'l5r5e.twenty_questions.part4.distinction'}}
|
||||
{{> 'systems/l5r5e/templates/actors/character/twenty-questions-item.html' itemsList=cache.step9.distinction stepName='step9.distinction' itemType='peculiarities' hideDndAt=1}}
|
||||
</label>
|
||||
<h3>{{localize (localize 'l5r5e.twenty_questions.part4.q10{suffix}' suffix=suffix)}}</h3>
|
||||
<h3>{{localizeEmbedded 'l5r5e.twenty_questions.part4.q10{suffix}' suffix=suffix}}</h3>
|
||||
<textarea name="step10.difficulty">{{data.step10.difficulty}}</textarea>
|
||||
<label>
|
||||
{{localize 'l5r5e.twenty_questions.part4.adversity'}}
|
||||
{{> 'systems/l5r5e/templates/actors/character/twenty-questions-item.html' itemsList=cache.step10.adversity stepName='step10.adversity' itemType='peculiarities' hideDndAt=1}}
|
||||
</label>
|
||||
<h3>{{localize (localize 'l5r5e.twenty_questions.part4.q11{suffix}' suffix=suffix)}}</h3>
|
||||
<h3>{{localizeEmbedded 'l5r5e.twenty_questions.part4.q11{suffix}' suffix=suffix}}</h3>
|
||||
<textarea name="step11.calms">{{data.step11.calms}}</textarea>
|
||||
<label>
|
||||
{{localize 'l5r5e.twenty_questions.part4.passion'}}
|
||||
{{> 'systems/l5r5e/templates/actors/character/twenty-questions-item.html' itemsList=cache.step11.passion stepName='step11.passion' itemType='peculiarities' hideDndAt=1}}
|
||||
</label>
|
||||
<h3>{{localize (localize 'l5r5e.twenty_questions.part4.q12{suffix}' suffix=suffix)}}</h3>
|
||||
<h3>{{localizeEmbedded 'l5r5e.twenty_questions.part4.q12{suffix}' suffix=suffix}}</h3>
|
||||
<textarea name="step12.worries">{{data.step12.worries}}</textarea>
|
||||
<label>
|
||||
{{localize 'l5r5e.twenty_questions.part4.anxiety'}}
|
||||
{{> 'systems/l5r5e/templates/actors/character/twenty-questions-item.html' itemsList=cache.step12.anxiety stepName='step12.anxiety' itemType='peculiarities' hideDndAt=1}}
|
||||
</label>
|
||||
<h3>{{localize (localize 'l5r5e.twenty_questions.part4.q13{suffix}' suffix=suffix)}}</h3>
|
||||
<h3>{{localizeEmbedded 'l5r5e.twenty_questions.part4.q13{suffix}' suffix=suffix}}</h3>
|
||||
<textarea name="step13.most_learn">{{data.step13.most_learn}}</textarea>
|
||||
<table>
|
||||
<tr>
|
||||
@@ -376,13 +376,13 @@
|
||||
</article>
|
||||
<article class="tab parts part5" data-group="primary" data-tab="part5">
|
||||
<h2>{{localize 'l5r5e.twenty_questions.part5.title'}}</h2>
|
||||
<h3>{{localize (localize 'l5r5e.twenty_questions.part5.q14{suffix}' suffix=suffix)}}</h3>
|
||||
<h3>{{localizeEmbedded 'l5r5e.twenty_questions.part5.q14{suffix}' suffix=suffix}}</h3>
|
||||
<textarea name="step14.first_sight">{{data.step14.first_sight}}</textarea>
|
||||
<label>
|
||||
{{localize 'l5r5e.twenty_questions.part5.accoutrement'}}
|
||||
{{> 'systems/l5r5e/templates/actors/character/twenty-questions-item.html' itemsList=cache.step14.special_features stepName='step14.special_features' itemType='items' hideDndAt=1}}
|
||||
</label>
|
||||
<h3>{{localize (localize 'l5r5e.twenty_questions.part5.q15{suffix}' suffix=suffix)}}</h3>
|
||||
<h3>{{localizeEmbedded 'l5r5e.twenty_questions.part5.q15{suffix}' suffix=suffix}}</h3>
|
||||
<textarea name="step15.stress">{{data.step15.stress}}</textarea>
|
||||
{{#ifCond data.template '==' 'core'}}
|
||||
<h3>{{localize 'l5r5e.twenty_questions.part5.q16'}}</h3>
|
||||
@@ -395,16 +395,16 @@
|
||||
<button class="next" name="next" type="button">{{localize 'l5r5e.twenty_questions.bt_next'}} <i class='fas fa-arrow-right'></i></button>
|
||||
</article>
|
||||
<article class="tab parts part6" data-group="primary" data-tab="part6">
|
||||
<h2>{{localize (localize 'l5r5e.twenty_questions.part6.title{suffix}' suffix=suffix)}}</h2>
|
||||
<h2>{{localizeEmbedded 'l5r5e.twenty_questions.part6.title{suffix}' suffix=suffix}}</h2>
|
||||
{{#ifCond data.template '==' 'pow'}}
|
||||
<h3>{{localize (localize 'l5r5e.twenty_questions.part5.q16_pow' suffix=suffix)}}</h3>
|
||||
<h3>{{localize 'l5r5e.twenty_questions.part5.q16_pow'}}</h3>
|
||||
<textarea name="step16.relations">{{data.step16.relations}}</textarea>
|
||||
<label>
|
||||
{{localize 'l5r5e.twenty_questions.part5.object'}}
|
||||
{{> 'systems/l5r5e/templates/actors/character/twenty-questions-item.html' itemsList=cache.step16.item stepName='step16.item' itemType='items' hideDndAt=1}}
|
||||
</label>
|
||||
{{/ifCond}}
|
||||
<h3>{{localize (localize 'l5r5e.twenty_questions.part6.q17{suffix}' suffix=suffix)}}</h3>
|
||||
<h3>{{localizeEmbedded 'l5r5e.twenty_questions.part6.q17{suffix}' suffix=suffix}}</h3>
|
||||
<textarea name="step17.parents_pov">{{data.step17.parents_pov}}</textarea>
|
||||
{{#ifCond data.template '==' 'core'}}
|
||||
<label>
|
||||
@@ -424,7 +424,7 @@
|
||||
{{> 'systems/l5r5e/templates/actors/character/twenty-questions-item.html' itemsList=cache.step17.bond stepName='step17.bond' itemType='bonds' hideDndAt=1}}
|
||||
</label>
|
||||
{{/ifCond}}
|
||||
<h3>{{localize (localize 'l5r5e.twenty_questions.part6.q18{suffix}' suffix=suffix)}}</h3>
|
||||
<h3>{{localizeEmbedded 'l5r5e.twenty_questions.part6.q18{suffix}' suffix=suffix}}</h3>
|
||||
<textarea name="step18.heritage_name">{{data.step18.heritage_name}}</textarea>
|
||||
{{#ifCond data.template '==' 'core'}}
|
||||
<table>
|
||||
@@ -491,7 +491,7 @@
|
||||
</select>
|
||||
</label>
|
||||
{{/ifCond}}
|
||||
<h3>{{localize (localize 'l5r5e.twenty_questions.part6.q19{suffix}' suffix=suffix)}}</h3>
|
||||
<h3>{{localizeEmbedded 'l5r5e.twenty_questions.part6.q19{suffix}' suffix=suffix}}</h3>
|
||||
<textarea name="step19.firstname">{{data.step19.firstname}}</textarea>
|
||||
<button class="next" name="next" type="button">{{localize 'l5r5e.twenty_questions.bt_next'}} <i class='fas fa-arrow-right'></i></button>
|
||||
</article>
|
||||
@@ -499,7 +499,7 @@
|
||||
<h2>{{localize 'l5r5e.twenty_questions.part7.title'}}</h2>
|
||||
<div>
|
||||
<label>
|
||||
<h3>{{localize (localize 'l5r5e.twenty_questions.part7.q20{suffix}' suffix=suffix)}}</h3>
|
||||
<h3>{{localizeEmbedded 'l5r5e.twenty_questions.part7.q20{suffix}' suffix=suffix}}</h3>
|
||||
<textarea name="step20.death">{{data.step20.death}}</textarea>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
52
system/templates/compendium/filter-bar.html
Normal file
52
system/templates/compendium/filter-bar.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<div class="l5r5e filter-bar">
|
||||
|
||||
{{#if filtersToShow.rank}}
|
||||
<div class="flexrow l5r5e rank-filter number-filter">
|
||||
<label>{{localize 'l5r5e.compendium.filter.rank'}}:</label>
|
||||
{{#each ranks}}
|
||||
<a data-rank="{{this}}"
|
||||
data-tooltip="{{localize 'l5r5e.compendium.filter.rank'}} {{this}}">{{this}}</a>
|
||||
{{/each}}
|
||||
<a data-clear style="display:none" data-tooltip="{{localize 'l5r5e.compendium.filter.clear'}}">×</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if filtersToShow.rarity}}
|
||||
<div class="flexrow l5r5e rarity-filter number-filter">
|
||||
<label>{{localize 'l5r5e.compendium.filter.rarity'}}:</label>
|
||||
{{#each rarities}}
|
||||
<a data-rarity="{{this}}"
|
||||
data-tooltip="{{localize 'l5r5e.compendium.filter.rarity'}} {{this}}">{{this}}</a>
|
||||
{{/each}}
|
||||
<a data-clear style="display:none" data-tooltip="{{localize 'l5r5e.compendium.filter.clear'}}">×</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if filtersToShow.ring}}
|
||||
<div class="flexrow l5r5e ring-filter">
|
||||
<label>{{localize 'l5r5e.rings.label'}}:</label>
|
||||
{{#each rings}}
|
||||
<i data-ring="{{this}}"
|
||||
data-tooltip="{{localize (concat 'l5r5e.rings.' this)}}"
|
||||
class="i_{{this}}"></i>
|
||||
{{/each}}
|
||||
<a data-clear style="display:none" data-tooltip="{{localize 'l5r5e.compendium.filter.clear'}}">×</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if filtersToShow.source}}
|
||||
<div class="flexrow l5r5e source-filter">
|
||||
<l5r5e-multi-select name="filter-sources" {{#if hideDisabledOptions}}hidedisabledoptions{{/if}}>
|
||||
{{selectOptions sources localize=true}}
|
||||
</l5r5e-multi-select>
|
||||
</div>
|
||||
{{#if showPlayerView}}
|
||||
<button type="button" class="gm applyPlayerFilter"
|
||||
data-action="applyPlayerView"
|
||||
data-tooltip="{{localize 'l5r5e.multiselect.player_filter_tooltip'}}">
|
||||
{{localize 'l5r5e.multiselect.player_filter_label'}}
|
||||
</button>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
30
system/templates/compendium/l5r5e-index-partial.html
Normal file
30
system/templates/compendium/l5r5e-index-partial.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{{!--
|
||||
L5R5e entry partial — mirrors Foundry's index-partial.hbs structure and adds
|
||||
ring/rarity/rank badges from entryFilterData injected by _prepareDirectoryContext.
|
||||
|
||||
NOTE: Foundry's index-partial.hbs renders a complete <li> element so it cannot
|
||||
be composed via {{> partial}} without nesting <li> inside <li>. We therefore
|
||||
duplicate its simple structure here and own it ourselves. If Foundry changes
|
||||
their index-partial.hbs, this file should be checked for parity.
|
||||
|
||||
Foundry original: templates/sidebar/apps/compendium/index-partial.hbs
|
||||
--}}
|
||||
<li class="directory-item entry document {{ @root.documentCls }} flexrow" data-entry-id="{{ _id }}">
|
||||
{{#if thumb}}
|
||||
<img class="thumbnail" src="{{ thumb }}" alt="{{ name }}" loading="lazy">
|
||||
{{else if img}}
|
||||
<img class="thumbnail" src="{{ img }}" alt="{{ name }}" loading="lazy">
|
||||
{{else}}
|
||||
<i class="{{ @root.sidebarIcon }}" inert></i>
|
||||
{{/if}}
|
||||
<a class="entry-name ellipsis" data-action="activateEntry">{{ name }}</a>
|
||||
{{#with (lookup @root.entryFilterData _id)}}
|
||||
{{#if (or ring rarity rank)}}
|
||||
<div class="l5r5e ring-rarity-rank" data-action="activateEntry">
|
||||
{{#if ring}} <i class="i_{{ring}}"></i>{{/if}}
|
||||
{{#if rarity}}<a>{{localize "l5r5e.sheets.rarity"}} {{rarity}}</a>{{/if}}
|
||||
{{#if rank}}<a>{{localize "l5r5e.sheets.rank"}} {{rank}}</a>{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/with}}
|
||||
</li>
|
||||
@@ -1,6 +0,0 @@
|
||||
<div class="flexrow l5r5e {{type}}-filter number-filter">
|
||||
<label>{{localize (localize 'l5r5e.compendium.filter.{type}' type=type)}}:</label>
|
||||
{{#each number}}
|
||||
<a data-{{../type}}="{{.}}" data-tooltip="{{localize (localize 'l5r5e.compendium.filter.{type}' type=../type)}} {{.}}">{{.}}</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
@@ -1,8 +0,0 @@
|
||||
<div class="flexrow l5r5e ring-filter number-filter">
|
||||
<label>{{localize 'l5r5e.rings.label'}}:</label>
|
||||
<i data-ring="fire" data-ringId="1" data-tooltip="{{localize 'l5r5e.rings.fire'}}" class="i_fire"></i>
|
||||
<i data-ring="water" data-ringId="2" data-tooltip="{{localize 'l5r5e.rings.water'}}" class="i_water"></i>
|
||||
<i data-ring="earth" data-ringId="3" data-tooltip="{{localize 'l5r5e.rings.earth'}}" class="i_earth"></i>
|
||||
<i data-ring="air" data-ringId="4" data-tooltip="{{localize 'l5r5e.rings.air'}}" class="i_air"></i>
|
||||
<i data-ring="void" data-ringId="5" data-tooltip="{{localize 'l5r5e.rings.void'}}" class="i_void"></i>
|
||||
</div>
|
||||
@@ -1,6 +0,0 @@
|
||||
<div class="l5r5e ring-rarity-rank">
|
||||
<i {{#if ring}} class="i_{{ring}}" {{/if}}>
|
||||
{{#if rarity}} {{localize "l5r5e.sheets.rarity"}} {{rarity}} {{/if}}
|
||||
{{#if rank}} {{localize "l5r5e.sheets.rank"}} {{rank}} {{/if}}
|
||||
</i>
|
||||
</div>
|
||||
@@ -1,4 +1,5 @@
|
||||
<form class="l5r5e dice-picker-dialog" autocomplete="off">
|
||||
{{> 'systems/l5r5e/templates/actors/character/effects.html'}}
|
||||
<table>
|
||||
{{!-- First line--}}
|
||||
<tr>
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
{{/if}}
|
||||
</li>
|
||||
</ul>
|
||||
{{> 'systems/l5r5e/templates/actors/character/effects.html' actor=l5r5e.actor}}
|
||||
</header>
|
||||
<section class="rnk-ct">
|
||||
{{!-- Body --}}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
<ul class="encounter">
|
||||
{{#each encounterTypeList}}
|
||||
<li class="encounter encounter-control" data-id="{{this}}">
|
||||
<i class="fa fas encounter-icon-{{this}}{{#ifCond this '==' ../encounterType}} active{{/ifCond}}" title="{{localize (localize 'l5r5e.conflict.initiative.{id}' id=this)}}"></i>
|
||||
<i class="fa fas encounter-icon-{{this}}{{#ifCond this '==' ../encounterType}} active{{/ifCond}}" title="{{localizeEmbedded 'l5r5e.conflict.initiative.{id}' id=this}}"></i>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
<ul class="prepared">
|
||||
{{#each prepared as |prepared charType|}}
|
||||
<li class="encounter prepared-control" data-id="{{charType}}">
|
||||
<i class="fa fas prepared-icon-{{prepared}} prepared-{{charType}}" title="{{localize (localize 'l5r5e.conflict.initiative.prepared_{value}' value=prepared)}} ({{localize (localize 'l5r5e.character_types.{type}' type=charType)}})"></i>
|
||||
<i class="fa fas prepared-icon-{{prepared}} prepared-{{charType}}" title="{{localizeEmbedded 'l5r5e.conflict.initiative.prepared_{value}' value=prepared}} ({{localizeEmbedded 'l5r5e.character_types.{type}' type=charType}})"></i>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<a data-actor-uuid="{{actor.uuid}}" data-type="prepared" class="actor-modify-control">
|
||||
<i data-type="text" data-text="<h2>{{localize 'l5r5e.conflict.initiative.title'}} : {{localize (localize 'l5r5e.conflict.initiative.prepared_{value}' value=actor.isPrepared)}}</h2>" class="fa fas prepared-icon-{{actor.isPrepared}} {{#ifCond actor.isPrepared '==' 'false'}}badvalue{{/ifCond}} actor-infos-control"></i>
|
||||
<i data-type="text" data-text="<h2>{{localize 'l5r5e.conflict.initiative.title'}} : {{localizeEmbedded 'l5r5e.conflict.initiative.prepared_{value}' value=actor.isPrepared}}</h2>" class="fa fas prepared-icon-{{actor.isPrepared}} {{#ifCond actor.isPrepared '==' 'false'}}badvalue{{/ifCond}} actor-infos-control"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{#if actor.haveWeaponEquipped}}<i data-type="weapons" data-actor-uuid="{{actor.uuid}}" class="fas fa-fan actor-infos-control {{#if actor.haveWeaponReadied}}badvalue{{/if}}"></i>{{/if}}</td>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<form class="l5r5e gm-toolbox" autocomplete="off">
|
||||
<ul class="gm-tools-container">
|
||||
<li class="faded-ui gm_monitor" data-action="open_gm_monitor" title="{{localize 'l5r5e.gm.monitor.title'}}">
|
||||
<li class="gm_monitor" data-action="open_gm_monitor" title="{{localize 'l5r5e.gm.monitor.title'}}">
|
||||
<i class="fas fa-table"></i>
|
||||
</li>
|
||||
<li class="difficulty_hidden" data-action="toggle_hide_difficulty" title="{{localize 'l5r5e.gm.toolbox.difficulty_hidden'}}">
|
||||
<i class="faded-ui fa fa-eye{{#if difficultyHidden}}-slash{{/if}}"></i>
|
||||
<strong class="faded-ui difficulty" data-action="change_difficulty" title="{{localize 'l5r5e.gm.toolbox.difficulty'}}">{{difficulty}}</strong>
|
||||
<i class="fa fa-eye{{#if difficultyHidden}}-slash{{/if}}"></i>
|
||||
<strong class="difficulty" data-action="change_difficulty" title="{{localize 'l5r5e.gm.toolbox.difficulty'}}">{{difficulty}}</strong>
|
||||
</li>
|
||||
<li class="faded-ui gm_actor_updates reset_void" data-action="reset_void" title="{{localize 'l5r5e.gm.toolbox.reset_void'}}">
|
||||
<li class="gm_actor_updates reset_void" data-action="reset_void" title="{{localize 'l5r5e.gm.toolbox.reset_void'}}">
|
||||
<i class="fas fa-podcast"></i>
|
||||
</li>
|
||||
<li class="faded-ui gm_actor_updates sleep" data-action="sleep" title="{{localize 'l5r5e.gm.toolbox.sleep'}}">
|
||||
<li class="gm_actor_updates sleep" data-action="sleep" title="{{localize 'l5r5e.gm.toolbox.sleep'}}">
|
||||
<i class="fa fa-bed"></i>
|
||||
</li>
|
||||
<li class="faded-ui gm_actor_updates scene_end" data-action="scene_end" title="{{localize 'l5r5e.gm.toolbox.scene_end'}}">
|
||||
<li class="gm_actor_updates scene_end" data-action="scene_end" title="{{localize 'l5r5e.gm.toolbox.scene_end'}}">
|
||||
<i class="fas fa-star-half-alt"></i>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<a data-actor-uuid="{{actor.uuid}}" class="actor-modify-control" data-action="toggle_prepared">
|
||||
<i data-type="text" data-text="<h2>{{localize 'l5r5e.conflict.initiative.title'}} : {{localize (localize 'l5r5e.conflict.initiative.prepared_{value}' value=actor.isPrepared)}}</h2>" class="fa fas prepared-icon-{{actor.isPrepared}} {{#ifCond actor.isPrepared '==' 'false'}}badvalue{{/ifCond}} actor-infos-control"></i>
|
||||
<i data-type="text" data-text="<h2>{{localize 'l5r5e.conflict.initiative.title'}} : {{localizeEmbedded 'l5r5e.conflict.initiative.prepared_{value}' value=actor.isPrepared}}</h2>" class="fa fas prepared-icon-{{actor.isPrepared}} {{#ifCond actor.isPrepared '==' 'false'}}badvalue{{/ifCond}} actor-infos-control"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{#if actor.haveWeaponEquipped}}<i data-type="weapons" data-actor-uuid="{{actor.uuid}}" class="fas fa-fan actor-infos-control {{#if actor.haveWeaponReadied}}badvalue{{/if}}"></i>{{/if}}</td>
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
{{#ifCond actor_type "==" "character"}}
|
||||
{{!-- 20Q --}}
|
||||
<ul>
|
||||
<li><b>{{localize (localize 'l5r5e.twenty_questions.part5.q14{suffix}' suffix=suffix)}}</b> : {{actorData.system.twenty_questions.step14.first_sight}}</li>
|
||||
<li><b>{{localize (localize 'l5r5e.twenty_questions.part5.q15{suffix}' suffix=suffix)}}</b> : {{actorData.system.twenty_questions.step15.stress}}</li>
|
||||
<li><b>{{localize (localize 'l5r5e.twenty_questions.part7.q20{suffix}' suffix=suffix)}}</b> : {{actorData.system.twenty_questions.step20.death}}</li>
|
||||
<li><b>{{localizeEmbedded 'l5r5e.twenty_questions.part5.q14' suffix}}</b> : {{actorData.system.twenty_questions.step14.first_sight}}</li>
|
||||
<li><b>{{localizeEmbedded 'l5r5e.twenty_questions.part5.q15' suffix}}</b> : {{actorData.system.twenty_questions.step15.stress}}</li>
|
||||
<li><b>{{localizeEmbedded 'l5r5e.twenty_questions.part7.q20' suffix}}</b> : {{actorData.system.twenty_questions.step20.death}}</li>
|
||||
</ul>
|
||||
{{/ifCond}}
|
||||
<p>{{{actorData.enrichedHtml.description}}}</p>
|
||||
|
||||
45
system/templates/hud/tactical-grid-ruler.html
Normal file
45
system/templates/hud/tactical-grid-ruler.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<div class="waypoint-label vertical {{cssClass}}">
|
||||
<div>
|
||||
{{#if action.icon}}
|
||||
<i class="icon {{action.icon}}"></i>
|
||||
{{else if action.label}}
|
||||
<label class="action-label">Action: {{localize action.label}}</label>
|
||||
{{/if}}
|
||||
{{#if cost}}
|
||||
<span class="total-measurement">{{cost.total}}</span>
|
||||
{{#if cost.delta}}
|
||||
<span class="delta-measurement">Cost Delta: ({{cost.delta}})</span>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<span class="total-measurement">{{distance.total}} {{units}}</span>
|
||||
{{#if distance.delta}}
|
||||
<span class="delta-measurement">Total Measure: ({{distance.delta}})</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if (and elevation (not elevation.hidden))}}
|
||||
<i class="icon {{elevation.icon}}"></i>
|
||||
<span class="total-elevation">{{elevation.total}} {{units}}</span>
|
||||
{{#if elevation.delta}}
|
||||
<span class="delta-elevation">({{elevation.delta}})</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if secret}}
|
||||
<i class="icon fa-solid fa-eye-slash"></i>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if additional}}
|
||||
<div class="waypoint-label-additional {{additional.cssClass}}">
|
||||
{{#if additional.icon}}
|
||||
<i class="icon {{additional.icon}}"></i>
|
||||
{{/if}}
|
||||
<span class="waypoint-label-text">{{additional.label}} {{additional.cost}}</span>
|
||||
{{#if additional.imgs.length}}
|
||||
{{#each additional.imgs as |img|}}
|
||||
<img class="icon" src="{{img}}">
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<img class="icon" src="{{additional.img}}">
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -1,6 +1,6 @@
|
||||
{{#each data.splitItemsList as |cat type|}}
|
||||
<h3 class="toggle-on-click" data-toggle="inventory-item-list-{{type}}">
|
||||
{{localize (localize 'l5r5e.{type}s.title' type=type)}} ({{cat.length}})
|
||||
{{localizeEmbedded 'l5r5e.{type}s.title' type=type}} ({{cat.length}})
|
||||
{{#if ../data.editable_not_soft_locked}}
|
||||
<a data-item-type="{{type}}" class="item-control item-add" title="{{localize 'l5r5e.global.add'}}"><i class="fas fa-plus"></i></a>
|
||||
{{/if}}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<strong>{{localize 'l5r5e.rings.label'}}</strong> : {{localizeRing data.system.ring}}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{localize 'l5r5e.sheets.types'}}</strong> : {{localize (localize 'l5r5e.peculiarities.types.{type}' type=data.system.peculiarity_type)}}
|
||||
<strong>{{localize 'l5r5e.sheets.types'}}</strong> : {{localizeEmbedded 'l5r5e.peculiarities.types.{type}' type=data.system.peculiarity_type}}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{localize 'l5r5e.advancements.curriculum'}}</strong> : {{localizeYesNo data.system.in_curriculum}}
|
||||
|
||||
37
system/templates/settings/tactical-grid-settings.html
Normal file
37
system/templates/settings/tactical-grid-settings.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<section class="standard-form scrollable">
|
||||
{{!-- GM-only: Enable/Disable Tactical Grid --}}
|
||||
{{#if isGm}}
|
||||
<fieldset>
|
||||
{{formGroup tactical_grid_enabled.field value=tactical_grid_enabled.value localize=true}}
|
||||
</fieldset>
|
||||
{{/if}}
|
||||
|
||||
{{!-- Range Band Configuration --}}
|
||||
<div class="range_band">
|
||||
{{#each rangeBands as |band|}}
|
||||
<fieldset>
|
||||
<legend>
|
||||
{{localize "l5r5e.tactical_grid.range_band" band=band.worldField.name}}
|
||||
</legend>
|
||||
|
||||
{{!-- GM-only: Range start distance --}}
|
||||
{{#if @root.isGm}}
|
||||
{{formGroup band.worldField.fields.start
|
||||
value=band.worldValue.start
|
||||
localize=true
|
||||
type="number"
|
||||
readonly=(eq band.index 0)}}
|
||||
{{/if}}
|
||||
|
||||
{{!-- Client: Visual settings --}}
|
||||
{{formGroup band.clientFields.fields.color
|
||||
value=band.clientValue.color
|
||||
localize=true}}
|
||||
|
||||
{{formGroup band.clientFields.fields.alpha
|
||||
value=band.clientValue.alpha
|
||||
localize=true}}
|
||||
</fieldset>
|
||||
{{/each}}
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,7 +1,9 @@
|
||||
# DicePicker (DP)
|
||||
|
||||
The DicePicker is the entry point to any L5R roll (but chat command).
|
||||
|
||||
## Usage example
|
||||
|
||||
```js
|
||||
new game.l5r5e.DicePickerDialog({
|
||||
skillId: 'aesthetics',
|
||||
@@ -11,8 +13,9 @@ new game.l5r5e.DicePickerDialog({
|
||||
```
|
||||
|
||||
## Constructor Options
|
||||
|
||||
| Property | Type | Notes / Examples |
|
||||
|------------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
|:---------|:-----|:-----------------|
|
||||
| actor | Actor | Any `Actor` object instance.<br>ex : `game.user.character`, `canvas.tokens.controlled[0].actor` |
|
||||
| actorId | string | This is the `id` not the `uuid` of an actor.<br>ex : "AbYgKrNwWeAxa9jT" |
|
||||
| actorName | string | Careful this is case-sensitive.<br>ex : "Isawa Aki" |
|
||||
@@ -22,15 +25,15 @@ new game.l5r5e.DicePickerDialog({
|
||||
| item | Item | The object of technique or weapon used for this roll.<br>_Added in v1.9.0_ |
|
||||
| itemUuid | string | The `uuid` of technique or weapon used for this roll. Can be anything retrieved by `fromUuid()` or `fromUuidSync()`<br>_Added in v1.9.0_ |
|
||||
| ringId | string | If not provided, take the current stance of the actor if any.<br>ex : "fire", "water" |
|
||||
| skillId | string | Skill `id`<br>ex : "design", "aesthetics", "courtesy" |
|
||||
| skillId| string | Skill `id`<br>ex : "design", "aesthetics", "courtesy" |
|
||||
| skillCatId | string | Skill category `id`<br>ex : "artisan", "scholar" |
|
||||
| skillsList | string[] | `skillId`/`skillCatId` list coma separated.<br>Allow the player to select the skill used in a select<br>ex : "artisan,design" |
|
||||
| target | TokenDocument | The targeted Token<br>_Added in v1.9.0_ |
|
||||
|
||||
|
||||
All these properties are optional.
|
||||
|
||||
For `actor*` properties, the resolution is in this order :
|
||||
|
||||
1. `option.actor`
|
||||
2. `option.actorId`
|
||||
3. `option.actorName`
|
||||
|
||||
@@ -4,6 +4,7 @@ The RnK use `ChatMessage` to retrieve the roll, alter it, add the new message an
|
||||
> If you have any idea how to modify directly the ChatMessage and update it, let me know.
|
||||
|
||||
Usage :
|
||||
|
||||
```js
|
||||
new RollnKeepDialog(messageId).render(true);
|
||||
```
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# Roll
|
||||
|
||||
The roll use the `RollL5r5e` class, who store a lot of additional variables.
|
||||
|
||||
Here is a view of `<roll>.l5r5e` properties :
|
||||
Here is a view of `<roll>.l5r5e` properties:
|
||||
|
||||
```js
|
||||
actor: null, // actor instance
|
||||
dicesTypes: {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Snippets
|
||||
This page contains some useful code snippet for macro or so.
|
||||
|
||||
This page contains some useful code snippet for macros, etc.
|
||||
|
||||
## Foundry (core)
|
||||
|
||||
### Actor related
|
||||
|
||||
Some useful methods to get an actor document :
|
||||
|
||||
### Foundry (core)
|
||||
## Actor related
|
||||
Some useful methods to get a actor document :
|
||||
```js
|
||||
// By uuid
|
||||
actor = fromUuid("Actor.lQYEzLeDS5ndopJV"); // or `fromUuidSync()
|
||||
@@ -28,27 +31,30 @@ actors = Array.from(game.user.targets).map(t => t.actor);
|
||||
actor = game.user.character;
|
||||
```
|
||||
|
||||
### Document open sheet
|
||||
|
||||
## Document open sheet
|
||||
```js
|
||||
<document>.sheet?.render(true);
|
||||
```
|
||||
|
||||
### FrameViewer
|
||||
|
||||
Open an url in an embedded windows in FoundryVTT.
|
||||
|
||||
## FrameViewer
|
||||
Open an url in an embedded windows in FoundryVTT
|
||||
```js
|
||||
// url, options
|
||||
new FrameViewer("https://foundryvtt.wiki/", { title: "SIDEBAR.Wiki" }).render(true);
|
||||
```
|
||||
|
||||
|
||||
## L5R5e specific
|
||||
|
||||
### autocomplete
|
||||
|
||||
A basic autocomplete for text inputs
|
||||
|
||||
Parameters :
|
||||
```
|
||||
|
||||
```md
|
||||
@param {jQuery} html HTML content of the sheet.
|
||||
@param {string} name Html name of the input
|
||||
@param {string[]} list Array of string to display
|
||||
@@ -56,6 +62,7 @@ Parameters :
|
||||
```
|
||||
|
||||
Usage examples :
|
||||
|
||||
```js
|
||||
game.l5r5e.HelpersL5r5e.autocomplete(
|
||||
html,
|
||||
@@ -70,17 +77,19 @@ game.l5r5e.HelpersL5r5e.autocomplete(
|
||||
```
|
||||
|
||||
It produces two values that can be useful in some cases :
|
||||
|
||||
```js
|
||||
formData["autoCompleteListName"]; // "system.difficulty"
|
||||
formData["autoCompleteListSelectedIndex"]; // 0 <- 1st élément selected
|
||||
formData["autoCompleteListSelectedIndex"]; // 0 <- 1st element selected
|
||||
```
|
||||
|
||||
### Debounce
|
||||
|
||||
### debounce
|
||||
Isolated Debounce by Id
|
||||
|
||||
Parameters :
|
||||
```
|
||||
|
||||
```md
|
||||
@param {String} id Named id (namespace)
|
||||
@param {Function} callback Callback function
|
||||
@param {Number} timeout Wait time (default 500ms)
|
||||
@@ -89,6 +98,7 @@ Parameters :
|
||||
```
|
||||
|
||||
Usage examples :
|
||||
|
||||
```js
|
||||
// Basic usage, non leading
|
||||
game.l5r5e.HelpersL5r5e.debounce('appId', (text) => { console.log(text) })('my text');
|
||||
@@ -102,12 +112,13 @@ game.l5r5e.HelpersL5r5e.debounce(
|
||||
)();
|
||||
```
|
||||
|
||||
|
||||
### drawManyFromPack
|
||||
|
||||
Shortcut method to draw names to chat (private) from a table in compendium without importing it
|
||||
|
||||
Parameters :
|
||||
```
|
||||
|
||||
```md
|
||||
@param {String} pack Compendium name
|
||||
@param {String} tableName Table name/id in this compendium
|
||||
@param {String} retrieve How many draw we do (default "5")
|
||||
@@ -115,24 +126,28 @@ Parameters :
|
||||
@return {Promise<{RollTableDraw}>} The drawn results
|
||||
```
|
||||
|
||||
Usage examples :
|
||||
Usage examples:
|
||||
|
||||
```js
|
||||
game.l5r5e.HelpersL5r5e.drawManyFromPack("l5r5e.core-name-tables", "Japanese names (Village)", 5);
|
||||
```
|
||||
|
||||
|
||||
### migrateWorld
|
||||
|
||||
You can force to trigger the system migration by using :
|
||||
|
||||
```js
|
||||
game.l5r5e.migrations.migrateWorld({force: true});
|
||||
```
|
||||
|
||||
This will try to normalize the actor/items in the current loaded world.
|
||||
|
||||
|
||||
### sendToChat
|
||||
|
||||
Send the description of this `Document` (`BaseSheetL5r5e`, `JournalL5r5e`, `ItemL5r5e`) to chat.
|
||||
|
||||
Usage examples :
|
||||
|
||||
```js
|
||||
game.l5r5e.HelpersL5r5e.sendToChat(game.actors.getName("Soshi Yui"));
|
||||
```
|
||||
|
||||
@@ -1,36 +1,40 @@
|
||||
# Sockets API
|
||||
Here is the list of Socket's methods, most of them is design to be use internally by the system, but you can find some useful for your projects.
|
||||
|
||||
Here is the list of Socket's methods, most of them are designed to be use internally by the system, but you can find some useful ones for your projects.
|
||||
|
||||
## deleteChatMessage
|
||||
Delete ChatMessage by his `id`, the GM permission is required, so a GM need to be connected for this to work. Used in RnK.
|
||||
|
||||
Delete ChatMessage by his `id`, the GM permission is required, so a GM needs to be connected for this to work. Used in RnK.
|
||||
|
||||
Usage :
|
||||
|
||||
```js
|
||||
game.l5r5e.sockets.deleteChatMessage(messageId);
|
||||
```
|
||||
|
||||
|
||||
## refreshAppId
|
||||
|
||||
Refresh an application windows by his `id` (not `appId`). Used in RnK.
|
||||
<br>Ex : `l5r5e-twenty-questions-dialog-kZHczAFghMNYFRWe`, not `65`.
|
||||
|
||||
Usage :
|
||||
|
||||
```js
|
||||
game.l5r5e.sockets.refreshAppId(applicationId);
|
||||
```
|
||||
|
||||
|
||||
## updateMessageIdAndRefresh
|
||||
|
||||
Change the message in the selected application windows, and re-render the application to force the refresh. Used in RnK.
|
||||
|
||||
Usage :
|
||||
|
||||
```js
|
||||
game.l5r5e.sockets.refreshAppId(applicationId, messageId);
|
||||
```
|
||||
|
||||
|
||||
## openDicePicker
|
||||
|
||||
_Added in v1.9.0_
|
||||
|
||||
Remotely open the DicePicker (DP) on targeted Users/Actors if they are active users. Used in initiative roll.
|
||||
@@ -38,15 +42,15 @@ Remotely open the DicePicker (DP) on targeted Users/Actors if they are active us
|
||||
Arguments :
|
||||
|
||||
| Property | Type | Notes / Examples |
|
||||
|-----------|---------|----------------------------------------------------------------------------|
|
||||
|:----------|:--------|:-----------------|
|
||||
| users | User[] | Users list to trigger the DP (will be reduce to `id` for network perf.) |
|
||||
| actors | Actor[] | Actors list to trigger the DP (will be reduce to `uuid` for network perf.) |
|
||||
| dpOptions | Object | Any [DicePickerDialog.options](dicepicker.md#constructor-options) |
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
#### Fitness skill roll for the all combat targets
|
||||
|
||||
```js
|
||||
game.l5r5e.sockets.openDicePicker({
|
||||
actors: Array.from(game.user.targets).map(t => t.document.actor),
|
||||
@@ -58,6 +62,7 @@ game.l5r5e.sockets.openDicePicker({
|
||||
```
|
||||
|
||||
#### Initiative roll (skirmish) for all player's character who are in combat tracker
|
||||
|
||||
```js
|
||||
game.l5r5e.sockets.openDicePicker({
|
||||
actors: game.combat.combatants.filter(c => c.hasPlayerOwner && !c.isDefeated && !c.initiative).map(c => c.actor),
|
||||
@@ -70,6 +75,7 @@ game.l5r5e.sockets.openDicePicker({
|
||||
```
|
||||
|
||||
#### Melee skill roll with "fire" ring, pre-selected for all the selected tokens
|
||||
|
||||
```js
|
||||
game.l5r5e.sockets.openDicePicker({
|
||||
actors: canvas.tokens.controlled.map(t => t.actor),
|
||||
@@ -82,6 +88,7 @@ game.l5r5e.sockets.openDicePicker({
|
||||
```
|
||||
|
||||
#### Skill roll with skill list for all active players (but GM)
|
||||
|
||||
```js
|
||||
game.l5r5e.sockets.openDicePicker({
|
||||
users: game.users.players.filter(u => u.active && u.hasPlayerOwner),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Storage API
|
||||
|
||||
Client side volatile storage - Store things like collapsible state (a refresh will clean it).
|
||||
|
||||
This is accessible anytime on `game.l5r5e.storage`.
|
||||
@@ -6,14 +7,17 @@ Used in sheets to store some collapsible element state.
|
||||
|
||||
|
||||
## getAppKeys
|
||||
|
||||
Get list of active keys for this app
|
||||
|
||||
Parameters :
|
||||
```
|
||||
|
||||
```js
|
||||
@param {string} app application name
|
||||
```
|
||||
|
||||
Usage examples :
|
||||
|
||||
```js
|
||||
storeInfos = game.l5r5e.storage.getAppKeys("my-appid-namespace");
|
||||
|
||||
@@ -25,19 +29,23 @@ storeInfos = game.l5r5e.storage.getAppKeys("CharacterSheetL5r5e-Actor-Zca44Nv7yd
|
||||
// 'inventory-item-list-weapon'
|
||||
// ]
|
||||
```
|
||||
|
||||
A defined key is "active", else they won't appear.
|
||||
|
||||
|
||||
## toggleKey
|
||||
|
||||
Toggle a key for this app.
|
||||
|
||||
Parameters :
|
||||
```
|
||||
|
||||
```js
|
||||
@param {string} app application name
|
||||
@param {string} key Key name
|
||||
```
|
||||
|
||||
Usage examples :
|
||||
|
||||
```js
|
||||
game.l5r5e.storage.toggleKey("my-appid-namespace", "var-key-to-toggle");
|
||||
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
# System helping (Contribute)
|
||||
## Rules
|
||||
You are free to contribute and propose corrections, modifications after fork. Try to respect theses rules:
|
||||
- Make sure you are up-to-date with the referent branch (most of the time the `dev` branch).
|
||||
- Clear and precise commit messages allow a quick review of the code.
|
||||
- If possible, limit yourself to one Feature per Merge request so as not to block the process.
|
||||
You are free to contribute and propose corrections and modifications after forking the repository. Try to respect these rules:
|
||||
|
||||
- Make sure you are up-to-date with the reference branch (generally this is the `dev` branch).
|
||||
- Clear and precise commit messages allow a quick review of the code. For fixes, recommend the testing steps for a reviewer to properly test the fix.
|
||||
- If possible, limit yourself to one Feature per Merge request to avoid slowing down the review process with multiple feature commits.
|
||||
|
||||
## Dev install
|
||||
1. Clone the repository.
|
||||
2. Use `npm ci` to install the dependence.
|
||||
3. Create a link from `<repo>/system` to your foundry system data (by default `%localappdata%/FoundryVTT/data/systems/l5r5e`).
|
||||
## Dev Install
|
||||
|
||||
1. Clone the repository to your local machine.
|
||||
2. Ensure that [Node.js](https://nodejs.org/en/download/) is installed.
|
||||
3. Use `npm ci` to install the relevant dependencies.
|
||||
4. Create a link from `<repo>/system` to your foundry system data (by default `%localappdata%/FoundryVTT/data/systems/l5r5e`).
|
||||
|
||||
Windows example (modify the target and source directories, and run this in administrator):
|
||||
|
||||
Windows example (modify the target and source directories, and run this in administrator) :
|
||||
```bash
|
||||
mklink /D /J "%localappdata%/FoundryVTT/data/systems/l5r5e" "D:/Projects/FVTT/l5r5e/system"
|
||||
```
|
||||
|
||||
|
||||
## Compiling SCSS
|
||||
1. Run `npm run watch` to watch and compile the `scss` files.
|
||||
|
||||
1. Run `npm run watch` to watch and compile the `*.scss` files.
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
# L5R5e System Wiki
|
||||
|
||||
## For users
|
||||
|
||||
- [Installation and modules](users/install.md)
|
||||
- [Updating - Bests practices](users/updating.md)
|
||||
- [Basic usage and filling sheets](users/basic-usage.md)
|
||||
- [Dice Rolling](users/dice.md)
|
||||
- [Symbols replacement list](users/symbols.md)
|
||||
- [Advanced : Techniques skill and difficulty syntaxe](users/techniques-syntaxe.md)
|
||||
- [Advanced : Techniques skill and difficulty syntaxe](users/techniques-syntax.md)
|
||||
- [Advanced : Custom Compendiums](users/custom-compendiums.md)
|
||||
- [Advanced : Using CUB for modifiers](users/cub-modifiers.md)
|
||||
|
||||
## For developers
|
||||
|
||||
- [System helping (Contribute)](dev/system-helping.md)
|
||||
- [Snippets](dev/snippets.md)
|
||||
- [Sockets API](dev/sockets.md)
|
||||
|
||||
@@ -1,7 +1,35 @@
|
||||
# Basic usage and filling sheets
|
||||
Mostly all the interactions are done with drag-n-drop.
|
||||
|
||||
Most interactions are done via drag and drop. You will need to activate the appropriate `Techniques` to allow you to place them in the sheet slots.
|
||||
|
||||
## Creating a Character (PC)
|
||||
Go in actor tab, and create a new `character`.
|
||||
|
||||
> TODO
|
||||
In the `Actor` tab, you can create a new `Character` by clicking the add actor button and using the `Player Character` sheet format. Generally, the Game Master (GM) will have to complete this step for you and assign you ownership before you can modify the character sheet.
|
||||
|
||||
1. You can build the character manually following the game system 20 questions sheet *or* you can use the `20Q` link at the top of the sheet and configure it with the 20 question output. Most of the steps below should ben completed automatically if you used the `20Q` feature.
|
||||
2. Fill in your `Honor`, `Glory` and `Status` stats. Increase your rings to the appropriate level for your character build. Your `Endurance`, `Composure`, `Focus` and `Vigilance` stats should all auto-calculate based on your ring bonus.
|
||||
3. Add any additional skill points in each of the `Artisan`, `Martial`, `Scholar`, `Social` or `Trade` skill groups.
|
||||
4. Select your allowed technique categories. You can select new techniques by dragging and dropping them from the L5R5e system compendiums under `L5R5e` > `Techniques` > selection of the appropriate technique category.
|
||||
5. Apply your school ability from the `School Abilities` compendium and a `Signature Scroll`, if appropriate.
|
||||
6. Ensure your `Ninjo` and `Giri` have been defined. Select `Distinctions`, `Passions` and `Bonds` from the `L5R5e` > `Character Related` section of the compendium.
|
||||
7. Define the `Paramount` and `Less Significant` Bushido aspects of your clan and any additional notes or character description in the respective fields.
|
||||
8. In the `Inventory` screen, you can drag elements from the `L5R5e` > `Objects related` section for `Items`, `Armors` and `Weapons` to add them to your character sheet.
|
||||
|
||||
> **Note**: Several of the default weapons and armors that are on your characters may not be available in the compendium. Simply drag the closest description item you can find and rename and change the stats appropriately once it's been placed in your character sheet.
|
||||
|
||||
9. Add `Item Patterns` from the `Item Patterns` compendium, if appropriate.
|
||||
|
||||
## Creating an NPC
|
||||
|
||||
Non-player Characters (NPCs) come in two flavors: minions and adversaries. While they differ in L5R in terms of power and influence on the narrative, they are basically built the same way in the Non-Player Character sheet.
|
||||
|
||||
1. Define the `Combat` and `Intrigue` difficulty, as appropriate, at the top of the sheet.
|
||||
2. Similar to the PCs, you will define `Honor`, `Glory`, `Status`, and Ring values for each character. Unlike players, you will need to manually define `Endurance`, `Composure`, `Focus` and `Vigilance`.
|
||||
3. Determine the demeanor for the character, and fill that in the `Demeanor` section. Modify the ring values based on the demeanor below the box. Acceptable values are an integer (i.e., 1, 2, 3, etc.) or an integer with a negative number (i.e., -1, -2, -3, etc.). Do not use `+` symbols in these fields.
|
||||
4. Define the general skill values in the `Artisan`, `Martial`, `Scholar`, `Social`, and `Trade` boxes. Unlike player characters, the skills are generalized for NPCs.
|
||||
5. The `Narrative` tab is filled out similarly to players. Note that most of the Distinctions and Passions of NPCs do not have a compendium equivalent. You can either build a custom compendium of these or manually define them by click the `+` button above each section to add and modify them.
|
||||
6. The inventory tab is filled out in much the same way as players.
|
||||
|
||||
## Creating an Army
|
||||
|
||||
> Todo
|
||||
@@ -1,18 +1,20 @@
|
||||
# Using CUB for Modifiers
|
||||
|
||||
> ⚠ ***Warning***: This module is outdated and the project has been abandoned. If you are using more current versions of Foundry (v11+), it is **not** recommended that you continue to use *Combat Utility Belt*.
|
||||
|
||||
# Using Combat Utility Belt (CUB) for Modifiers
|
||||
|
||||
> ⚠ The module [Combat Utility Belt](https://foundryvtt.com/packages/combat-utility-belt) is required.
|
||||
|
||||
|
||||
## Attributes modifiers
|
||||
Replace `<attribute>` with actual attribute (i.e. `endurance`, `vigilance`, `focus`, `composure`) and `<number>` with actual number to be added.
|
||||
Replace `<attribute>` with the actual system attribute (i.e. `endurance`, `vigilance`, `focus`, `composure`) and `<number>` with actual number to be added.
|
||||
|
||||
When setup in CUB this would modify PC derived attributes to increase or reduce them by the number given.
|
||||
|
||||
Allows automating certain invocations and item effects (such as the cursed Kama from Sins of Regret supplement).
|
||||
When setup in CUB this would modify PC-derived attributes to increase or reduce them by the number given.
|
||||
|
||||
This allows automating certain invocations and item effects (such as the cursed Kama from Sins of Regret supplement).
|
||||
|
||||
### For `character` type
|
||||
Syntaxe:
|
||||
|
||||
Syntax:
|
||||
> system.modifiers.character.`<attribute>` += `<number>`
|
||||
|
||||
Examples:
|
||||
@@ -21,29 +23,29 @@ Examples:
|
||||
|
||||
|
||||
### For `adversary` or `minion` types
|
||||
Syntaxe:
|
||||
|
||||
Syntax:
|
||||
> system.`<attribute>` += `<number>`
|
||||
|
||||
Exemples:
|
||||
Examples:
|
||||
> system.vigilance += 1 // add 1
|
||||
> <br>system.composure += -2 // remove 2
|
||||
|
||||
|
||||
|
||||
## Rings/Skills modifiers
|
||||
|
||||
Both PCs and NPCs can have their skills and rings increased as well by conditions (should you wish to ignore some of the RAW).
|
||||
|
||||
Syntaxe:
|
||||
Syntax:
|
||||
> system.rings.`<ring>` += `<number>`
|
||||
> <br>system.skills.`<skillGroup>`.`<skill>` += `<number>` // for PCs
|
||||
> <br>system.skills.`<skillGroup>` += `<number>` // for NPCs
|
||||
|
||||
Exemples:
|
||||
Examples:
|
||||
> system.rings.earth += 1
|
||||
> <br>system.skills.artisan.aesthetics += 1 // for PCs
|
||||
> <br>system.skills.martial += -1 // for NPCs
|
||||
|
||||
|
||||
The above need to be setup as conditions using CUB at the moment so that they can be added/removed as required.
|
||||
The above has to be set up as conditions using CUB at the moment so that they can be added/removed as required.
|
||||
|
||||
Regarding skills and rings modifiers, I believe you would need to remove them temporarily for advancements as it might cause extra XP to be spent, but yet to test it fully.
|
||||
Regarding skills and rings modifiers, you may need to remove them temporarily for advancements as it might cause extra XP to be spent. This has not been fully tested.
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
# Compendiums
|
||||
**Never directly edit the system compendiums**.
|
||||
They will be erased anytime you update the system.
|
||||
|
||||
The best option to keep the links between items and properties for example, is to fill a custom compendium using Babele.
|
||||
This way, the system compendiums will be overridden by this module.
|
||||
**Never directly edit the system compendiums**. They will be erased anytime you update the system.
|
||||
|
||||
The best option to keep the links between items and properties, for example, is to fill out a custom compendium using Babele. This will prevent changes you make to system compendiums from being overridden by game system updates.
|
||||
|
||||
## Using the Custom Compendiums Maker (recommended)
|
||||
|
||||
Here is a quick guide to fill the compendiums with Custom Compendium Maker.
|
||||
|
||||
1. Go to the [Custom Compendiums Maker Website](https://vly.yn.lu/l5r5e-custom-compendium-maker/)
|
||||
@@ -14,7 +13,7 @@ Here is a quick guide to fill the compendiums with Custom Compendium Maker.
|
||||
|
||||

|
||||
|
||||
3. Open the template file with any software who can open and write an Excel sheet (.xlsx).
|
||||
3. Open the template file with any software that can modify an Excel sheet (.xlsx).
|
||||
4. Check instruction on `Infos` sheet.
|
||||
5. Go into the `Configuration` sheet, and changes the values in the `Values` column.<br>If your language is `English` you'll probably only need to change the `Author` value.
|
||||
|
||||
@@ -24,8 +23,8 @@ Here is a quick guide to fill the compendiums with Custom Compendium Maker.
|
||||
|
||||

|
||||
|
||||
7. Regularly save the file !
|
||||
8. When it's done, visit the website again and follow the instructions.
|
||||
7. Make sure you save the file often!
|
||||
8. When it's done, visit the website again and follow the instructions:
|
||||
|
||||

|
||||
|
||||
@@ -39,21 +38,21 @@ Here is a quick guide to fill the compendiums with Custom Compendium Maker.
|
||||

|
||||
|
||||
11. Open FoundryVTT and load an L5R5e World.
|
||||
12. Make sure to have all these modules enabled :
|
||||
12. Make sure to have all these modules enabled:
|
||||
- Babele
|
||||
- L5R5e Custom Compendiums
|
||||
- LibWrapper
|
||||
|
||||

|
||||
|
||||
13. Open an item you know you had added a description to see the result :
|
||||
13. Open an item you know you had added a description to see the result:
|
||||
|
||||

|
||||
|
||||
14. Done, have a good time playing L5R !
|
||||
14. Done, have a good time playing L5R!
|
||||
|
||||
|
||||
## Using the Custom Compendiums Module
|
||||
You will need to manually download the following module, and edit json files.
|
||||
You will need to manually download the following module and edit json files.
|
||||
Please follow the instructions on :
|
||||
> https://gitlab.com/teaml5r/l5r5e-custom-compendiums
|
||||
|
||||
@@ -1,49 +1,60 @@
|
||||
# Installation
|
||||
|
||||
## System Installation
|
||||
### With Search (recommended)
|
||||
|
||||
### Installation directly on Foundry with search (Recommended)
|
||||
|
||||
1. Open FoundryVTT.
|
||||
2. In the `Game Systems` tab, clic `Install system`.
|
||||
3. Search `L5R`, on the line `Legend of the Five Rings (5th Edition)`, clic `Install`.
|
||||
2. In the `Game Systems` tab, click `Install system`.
|
||||
3. Search for `L5R`, on the line `Legend of the Five Rings (5th Edition)`, click `Install`.
|
||||
|
||||
### Installation on Forge
|
||||
|
||||
1. Navigate to the `Systems` section in the Bazaar.
|
||||
2. Search for `L5R` on the filter line. **Note**: Install `Legend of the Five Rings (5th Edition)` **not** `Legend of the Five Rings (5E)` due to it being deprecated
|
||||
3. Once installed, you can now start a Forge instance with it installed.
|
||||
|
||||
### With the manifest
|
||||
1. Open FoundryVTT.
|
||||
2. In the `Game Systems` tab, clic `Install system`.
|
||||
3. Copy this link and use it in the `Manifest URL`, then clic `Install`.
|
||||
> https://gitlab.com/teaml5r/l5r5e/-/raw/master/system/system.json
|
||||
|
||||
1. Open FoundryVTT.
|
||||
2. In the `Game Systems` tab, click `Install system`.
|
||||
3. Copy this link and use it in the `Manifest URL`, then click `Install`: [https://gitlab.com/teaml5r/l5r5e/-/raw/master/system/system.json](https://gitlab.com/teaml5r/l5r5e/-/raw/master/system/system.json)
|
||||
|
||||
## Modules
|
||||
L5R do not required a lot of module, i highly encourage you to start with a small number of it.
|
||||
|
||||
Some modules require others library/module, you need to install them to allow the primary module work.
|
||||
Nothing fancy, just accept when FoundryVTT prompt you to download or activate the dependencies.
|
||||
L5R does not require a lot of modules and it is highly encourage you to start with a small number of them.
|
||||
|
||||
Some modules require others libraries/modules and you need to install the dependency modules for them to work. Nothing fancy, just accept when FoundryVTT prompts you to download or activate the dependencies.
|
||||
|
||||
### Some recommended modules
|
||||
| Module name | Notes |
|
||||
|------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [Babele](https://foundryvtt.com/packages/babele) | Required for non english compendium translation |
|
||||
| [Ownership Viewer](https://foundryvtt.com/packages/permission_viewer) | Lets you see instantly who has permissions to see what item |
|
||||
| [Dice So Nice!](https://foundryvtt.com/packages/dice-so-nice) | Add 3D dices that bounce on the screen when you roll dice |
|
||||
| [Small Legend of the 5 Rings Tools](https://foundryvtt.com/packages/l5r-dragruler) | Series of tools for L5R |
|
||||
| [Search Anywhere](https://foundryvtt.com/packages/searchanywhere) | Don't spent too much time searching the right technique |
|
||||
| [FXMaster](https://foundryvtt.com/packages/fxmaster) | More effects |
|
||||
| [Scene Clicker](https://foundryvtt.com/packages/scene-clicker) | Clicking on a Scene or a Scene Link will now "view" the Scene instead of rendering the Scene Config Sheet |
|
||||
| [Universal Battlemap Importer](https://foundryvtt.com/packages/dd-import) | Allows Importing [DungeonDraft](https://dungeondraft.net/), [DungeonFog](https://www.dungeonfog.com/) or [Arkenforge](https://arkenforge.com/) export files into FoundryVTT |
|
||||
| [Compendium Folders](https://foundryvtt.com/packages/compendium-folders) | Add folders to compendiums |
|
||||
| [Chat Images](https://foundryvtt.com/packages/chat-images) | Lets you drag images into the chat, one of the quicker ways to do 'he looks like this' |
|
||||
| [Combat Utility Belt](https://foundryvtt.com/packages/combat-utility-belt) | A totally over-engineered but helpful app that will, among other things, let you set up custom statuses |
|
||||
| [Timer](https://foundryvtt.com/packages/timer) | A simple timer, useful to stress a little your players |
|
||||
The following is a list of recommended modules. Note that a module that is outdated isn't *necessarily* unusable; however, you may find integration/interaction errors with the more current versions of Foundry and generally Forge will not allow installations of modules that aren't verified for the installed version of Foundry if **Game Management** is enabled.
|
||||
|
||||
### Recommended Modules
|
||||
|
||||
| Module name | Notes | Module Status |
|
||||
| :------------ | :------ | :-------------- |
|
||||
| [Babele](https://foundryvtt.com/packages/babele) | Required for non-English compendium translations | Verified for v13+ |
|
||||
| [Ownership Viewer](https://foundryvtt.com/packages/permission_viewer) | Lets you see instantly who has permissions to see what item | Verified for v13+ |
|
||||
| [Dice So Nice!](https://foundryvtt.com/packages/dice-so-nice) | Add 3D dices that bounce on the screen when you roll | Verified for v13+ |
|
||||
| [Small Legend of the 5 Rings Tools](https://foundryvtt.com/packages/l5r-dragruler) | Series of tools for L5R | This module is no longer available |
|
||||
| [Search Anywhere](https://foundryvtt.com/packages/searchanywhere) | Don't spent too much time searching the right technique | This module is outdated (Verified v9). Alternative: [Spotlight Omnisearch](https://foundryvtt.com/packages/spotlight-omnisearch) |
|
||||
| [FXMaster](https://foundryvtt.com/packages/fxmaster) | More effects | Verified for v13+ |
|
||||
| [Scene Clicker](https://foundryvtt.com/packages/scene-clicker) | Clicking on a Scene or a Scene Link will now "view" the Scene instead of rendering the Scene Config Sheet | This module is outdated (verified v9). Alternative: [Monk's Scene Navigation](https://foundryvtt.com/packages/monks-scene-navigation) |
|
||||
| [Universal Battlemap Importer](https://foundryvtt.com/packages/dd-import) | Allows Importing [DungeonDraft](https://dungeondraft.net/), [DungeonFog](https://www.dungeonfog.com/) or [Arkenforge](https://arkenforge.com/) export files into FoundryVTT | Verified for v13+ |
|
||||
| [Compendium Folders](https://foundryvtt.com/packages/compendium-folders) | Add folders to compendiums | This module is outdated (Verified v11). Generally not needed since folder support is now enabled.
|
||||
| [Chat Images](https://foundryvtt.com/packages/chat-images) | Lets you drag images into the chat, one of the quicker ways to do 'he looks like this' | Verified for v13+ |
|
||||
| [Combat Utility Belt](https://foundryvtt.com/packages/combat-utility-belt) | A totally over-engineered but helpful app that will, among other things, let you set up custom statuses | This project is abandoned and has multiple integration errors with v13. Use at your own risk. |
|
||||
| [Timer](https://foundryvtt.com/packages/timer) | A simple timer, useful to stress your players a bit. | Verified for v13+ |
|
||||
|
||||
### Map Module
|
||||
|
||||
The official 5e Rokugan map is publish under the module section in FoundryVTT. It is also available for download on Forge.
|
||||
|
||||
### Map module
|
||||
The official 5e Rokugan map is publish under the module section in FoundryVTT.
|
||||
- [L5R5e - Rokugan map for Legend of the Five Rings (5th edition)](https://foundryvtt.com/packages/l5r5e-map)
|
||||
|
||||
|
||||
## Worlds
|
||||
|
||||
We have published the official free content in form of worlds ready to play :
|
||||
|
||||
- [L5R5E - Cresting Waves](https://foundryvtt.com/packages/l5r5e-world-waves)
|
||||
- [L5R5E - In the Palace of the Emerald Champion](https://foundryvtt.com/packages/l5r5e-world-palace)
|
||||
- [L5R5E - The Highwayman](https://foundryvtt.com/packages/l5r5e-world-highwayman)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Symbols replacement list
|
||||
In sheets or journals, you can use these tags to use symbols :
|
||||
```
|
||||
|
||||
In sheets or journals, you can use these tags to embed a symbol in the text:
|
||||
|
||||
```md
|
||||
Dice symbols : (op) (su) (ex) (st) (skill) (ring)
|
||||
Rings : (earth) (water) (fire) (air) (void)
|
||||
Tech: (kiho) (maho) (ninjutsu) (ritual) (shuji) (invocation) (kata) (prereq) (inversion) (mantra)
|
||||
@@ -10,4 +12,4 @@ Others : (courtier) (bushi) (shugenja)
|
||||
|
||||
Result :
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Techniques skill and difficulty syntaxe
|
||||
# Techniques skill and difficulty syntax
|
||||
|
||||
On the Technique sheets, you will find two fields `Difficulty` and `Skill`.
|
||||
|
||||
These fields have special constraints, you will find theirs rules below.
|
||||
These fields have special constraints, with the following rules listed below:
|
||||
|
||||
## Difficulty
|
||||
Can be :
|
||||
|
||||
Valid values for this field are:
|
||||
|
||||
- A integer number : `1` to `9`.
|
||||
- Or specific syntax "@`S`:`prop1`" or "@`T`:`prop1`|`max`" or "@`T`:`prop1`|`max`(`prop2`)" :
|
||||
- `@` fixed, trigger the parser
|
||||
@@ -18,9 +21,10 @@ Can be :
|
||||
- `@T:vigilance|min` : Difficulty will be the `vigilance` from the target with the minimum vigilance (implicit) value. it's the same to wrote `@T:vigilance|min(vigilance)`.
|
||||
- `@T:vigilance|max(statusRank)` : Difficulty will be the `vigilance` from the target with the maximum `statusRank` value.
|
||||
|
||||
|
||||
## Skill
|
||||
Can be :
|
||||
|
||||
Valid values for this field are:
|
||||
|
||||
- Any `Skill` id : `melee`, `fitness`...
|
||||
- Any `SkillCategory` id : `scholar`, `martial`...
|
||||
- Or both in list, coma separated.
|
||||
@@ -1,4 +1,10 @@
|
||||
# Updating - Bests practices
|
||||
|
||||
- Anytime you update to a major version make a backup of foundry's data directory (default : `%localappdata%/FoundryVTT/data/`).
|
||||
- Take time to upgrading to a major version (ex FoundryVTT v9->v10).<br>A lot of bugs can be on firsts patchs, and a lots of systems/modules won't upgrade fast and will be incompatible or not tested (a lot of us do it only on our free time).<br>If you need some timing windows: let at least 2 weeks to 1 month.
|
||||
- Anytime you update to a major version:
|
||||
- **Foundry**: make a backup of foundry's data directory (default : `%localappdata%/FoundryVTT/data/`).
|
||||
- **Forge**: you will be prompted on Foundry upgrades (both major and minor) to download a local copy of the world for backup. It is recommended you complete this step to avoid data loss.
|
||||
- It is recommended that you wait for some time before a major upgrade (ex FoundryVTT v9 -> v10) when a new version is released. This are a number of reasons to avoid immediate upgrades:
|
||||
- Several modules your world uses will have a lot of bugs introduced that aren't fully corrected in the first few patches.
|
||||
- Several systems/modules won't update quickly, will be incompatible with the current Foundry version, or left untested (the developers for a many of modules and systems only make updates in their free time).
|
||||
- A solid recommendation for update delay should be somewhere in the range of 2 weeks to 1 month, if not longer. Generally, this is less of a concern with minor upgrades which don't change core systems in Foundry.
|
||||
- Make sure you check the compatibility of your world's modules with the new versions and potentially disable certain modules if there is little chance of a near-term update. A good tool to use for managing modules profiles is [Module Profiles](https://foundryvtt.com/packages/module-profiles) to ensure that game-breaking modules can be deactivated by having a master profile.
|
||||
|
||||
Reference in New Issue
Block a user