Compare commits

...

37 Commits

Author SHA1 Message Date
abe35cb537 First round of changes 2026-04-23 14:27:33 +02:00
f72230dd39 First round of changes 2026-04-23 14:27:27 +02:00
Vlyan
36a66d3eac Release v1.13.4 2026-03-01 12:39:48 +01:00
Vlyan
317411ce60 Sync languages files from English 2026-03-01 12:30:01 +01:00
Vlyan
3c6529bc99 Updated items from import 2026-03-01 12:04:57 +01:00
Litasa
061390df80 Edit CHANGELOG.md 2026-03-01 08:12:38 +00:00
Litasa
b4fd1c738f Updated CHANGELOG.md Release 1.13.4 2026-03-01 07:57:26 +00:00
Litasa
c7d6c6c5e5 Merge branch 'some_missing_items' into 'dev'
Updating compendiums:
* Adding some items for Starting Outfits that was not in the system
* Split Poison and Omamori into individual items since it makes sense
* Added the specific items contained in a "Travelers Pack"
* Added arrow types not added from core book

See merge request teaml5r/l5r5e!58
2026-03-01 07:39:24 +00:00
Litasa
31f094818e Updating compendiums:
* Adding some items for Starting Outfits that was not in the system
* Split Poison and Omamori into individual items since it makes sense (old items will now be generic)
* Added the specific items contained in a "Travelers Pack"
* Added arrow types not added from core book
2026-03-01 07:39:24 +00:00
Litasa
40afa53337 Merge branch 'better_select_for_combat_tracker' into 'dev'
Adding compendium style select circle for combat encounter. Removed styling...

See merge request teaml5r/l5r5e!62
2026-03-01 07:12:33 +00:00
Litasa
cb98d721c5 Adding compendium style select circle for combat encounter. Removed styling... 2026-03-01 07:12:32 +00:00
Litasa
6ba5137ea1 Added me (Litasa) to curret L5R team in README.md
Removed Carter from current L5R team.
2026-02-28 21:04:15 +00:00
Litasa
5377674a30 Merge branch 'issue_75_gm_toolbox_and_monitor_visibility_issues' into 'dev'
issue 75: Unable to see icons in GM ToolBox and text in GM Monitor

See merge request teaml5r/l5r5e!59
2026-02-28 20:58:16 +00:00
Litasa
f6ed462bce issue 75: Unable to see icons in GM ToolBox and text in GM Monitor 2026-02-28 20:58:16 +00:00
Litasa
890223021a Added Litasa as a maintainer 2026-02-27 18:14:43 +00:00
Litasa
b1f874b3c8 Merge branch 'refactor_compendium_pr' into 'dev'
Updating the compendium filter to make it more snappy

See merge request teaml5r/l5r5e!61
2026-02-27 04:15:11 +00:00
Litasa
2dd9ee19e9 Updating the compendium filter to make it more snappy 2026-02-27 04:15:10 +00:00
Litasa
aa203c546c Merge branch 'issue_74_wounded_condition_in_dice_picker' into 'dev'
Issue 74: Adding all active conditions to the top of dice-picker-dialog so you are...

See merge request teaml5r/l5r5e!60
2026-02-26 01:04:30 +00:00
Litasa
8f31031244 Issue 74: Adding all active conditions to the top of dice-picker-dialog so you are... 2026-02-26 01:04:30 +00:00
Litasa
84e367b79f Merge branch 'issue_64_next_rank_not_shown' into 'dev'
Issue 64: Fixing issue where the new rank did not show until after adding a new item....

See merge request teaml5r/l5r5e!57
2026-02-25 21:55:01 +00:00
Litasa
0854e25a66 Issue 64: Fixing issue where the new rank did not show until after adding a new item.... 2026-02-25 21:55:00 +00:00
Litasa
0c299db26f Merge branch 'issue_69_add_experience_button' into 'dev'
Issue 69: Adding incremental buttons to honor, glory and status. Renaming...

See merge request teaml5r/l5r5e!54
2026-02-25 21:39:21 +00:00
Litasa
f267d06536 Issue 69: Adding incremental buttons to honor, glory and status. Renaming... 2026-02-25 21:39:21 +00:00
Vlyan
494b027513 Merge branch 'issue72_missing_sidebar_icons' into 'dev'
Issue72: Missing Sidebar Icons

See merge request teaml5r/l5r5e!53
2026-02-25 08:08:10 +00:00
Litasa
35c58ff631 Issue72: Missing Sidebar Icons 2026-02-25 08:08:10 +00:00
Vlyan
e87cd6d73e Merge branch 'cruxis/update-wiki-content' into 'dev'
Cruxis/update wiki content

See merge request teaml5r/l5r5e!52
2026-02-25 08:05:13 +00:00
Norman Briggs
05b7a1181c Cruxis/update wiki content 2026-02-25 08:05:12 +00:00
Vlyan
2e91fe7ae4 Merge branch 'master' into 'dev'
Fix typo in English Courts of Stone title.

See merge request teaml5r/l5r5e!51
2026-02-25 08:03:34 +00:00
SagaTympana
9c3de358b3 Fix typo in English Courts of Stone title. 2026-02-06 13:03:01 -05:00
Vlyan
b1e73f0761 Release 1.13.3 2026-02-01 10:37:27 +01:00
Vlyan
222aa75a1d Added Translations for Tactical Grid, and some cleanup 2026-01-30 09:28:15 +01:00
Vlyan
caa78d7c45 Merge branch 'patch-1' into 'dev'
Title advancement auto-name/icon fix

See merge request teaml5r/l5r5e!50
2026-01-30 07:58:38 +00:00
SagaTympana
5edcaa373c Title advancement auto-name/icon fix 2026-01-30 07:58:38 +00:00
Vlyan
08e412b32f Merge branch 'tactical_range_bands_dev' into 'dev'
Tactical Grid Range Band

See merge request teaml5r/l5r5e!49
2026-01-07 13:31:28 +00:00
Litasa
4269946c30 Tactical Grid Range Band 2026-01-07 13:31:28 +00:00
Vlyan
607817302b Merge branch 'fix_demeanors_translation' into 'dev'
Update demeanors

See merge request teaml5r/l5r5e!48
2025-11-04 20:06:31 +00:00
Olivier Brencklé
dd39fa6113 Update demeanors 2025-11-04 20:06:30 +00:00
109 changed files with 5988 additions and 1103 deletions

1
.gitignore vendored
View File

@@ -13,3 +13,4 @@ system/l5r5e.lock
# v11 db files (will be added later) # v11 db files (will be added later)
system/packs/*/** system/packs/*/**
.github/

View File

@@ -6,6 +6,25 @@ Date format : day/month/year
> - `foundry-version`: Stick to the major version of FoundryVTT. > - `foundry-version`: Stick to the major version of FoundryVTT.
> - `system-version`: System functionalities and Fixes. > - `system-version`: System functionalities and Fixes.
## 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 ## 1.13.2 - 18/10/2025 - Conditions Icons & Fixes
- Fix Actor Sheet for pressing key `Enter` in input trigger `no active Encounter...` message. - 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)`. - Fix Compendium `Astrolab` is duplicate with `Mantis Clan` and `Children of the Five Winds`. Renamed the `cotfw` version to `Astrolabe (Unicorn)`.
@@ -14,8 +33,8 @@ Date format : day/month/year
- Spanish language updated thanks to Alejabarr. - Spanish language updated thanks to Alejabarr.
## 1.13.1 - 21/09/2025 - Conditions & Fixes ## 1.13.1 - 21/09/2025 - Conditions & Fixes
- Fix for Clicking on items doesn't show item window (#65 Thx to Litasa) - Fix for Clicking on items doesn't show item window (#65 Thx to Litasa).
- Fix for fade configuration (#66) - Fix for fade configuration (#66).
- Added some Tooltips loading optimizations (#62 Thanks to KitCat). - Added some Tooltips loading optimizations (#62 Thanks to KitCat).
- Added some Properties loading optimizations (#63 Thanks to KitCat). - Added some Properties loading optimizations (#63 Thanks to KitCat).
- Conditions changes : - Conditions changes :

View File

@@ -18,13 +18,14 @@ See the [Wiki page - Installation](https://gitlab.com/teaml5r/l5r5e/-/wikis/user
## Current L5R team (alphabetical order) ## Current L5R team (alphabetical order)
- Carter (compendiums, adventure adaptation) - Litasa (development)
- Vlyan (development) - Vlyan (development)
## Historical L5R team (alphabetical order) ## Historical L5R team (alphabetical order)
- Carter (compendiums, adventure adaptation) - Carter (compendiums, adventure adaptation)
- Hrunh (compendiums, pre-gen characters adaptation) - Hrunh (compendiums, pre-gen characters adaptation)
- Litasa (development)
- Mandar (development) - Mandar (development)
- Sasmira (contributor) - Sasmira (contributor)
- Vlyan (development) - Vlyan (development)

Binary file not shown.

4
package-lock.json generated
View File

@@ -1,11 +1,11 @@
{ {
"name": "l5r5e", "name": "l5rx-chiaroscuro",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "l5r5e", "name": "l5rx-chiaroscuro",
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {

View File

@@ -1,5 +1,5 @@
{ {
"name": "l5r5e", "name": "l5rx-chiaroscuro",
"version": "1.0.0", "version": "1.0.0",
"description": "This is a game system for Legend of the Five Rings (5th edition) by Edge Studio", "description": "This is a game system for Legend of the Five Rings (5th edition) by Edge Studio",
"main": "index.js", "main": "index.js",

View File

@@ -8,7 +8,7 @@
Created by potrace 1.15, written by Peter Selinger 2001-2017 Created by potrace 1.15, written by Peter Selinger 2001-2017
</metadata> </metadata>
<g transform="translate(0.000000,1264.000000) scale(0.100000,-0.100000)" <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 <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 -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 -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

View File

@@ -1,13 +1,20 @@
<?xml version="1.0" encoding="utf-8"?> <?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" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
<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" xml:space="preserve">
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"> <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"> <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> </style>
<g> <g>
<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 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.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 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.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 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"/> 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="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="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="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"/> 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 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,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 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

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <!-- 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" <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"> <style type="text/css">
.st0{fill:#1D1C1A;} .st0{fill:#030104;}
</style> </style>
<g> <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 <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

View File

@@ -262,7 +262,7 @@
} }
}, },
{ {
"id": "Journal of observations", "id": "Journal of Observations",
"name": "Journal d'observations", "name": "Journal d'observations",
"description": "", "description": "",
"source_reference": { "source_reference": {
@@ -972,6 +972,198 @@
"source_reference": { "source_reference": {
"page": "" "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 dentrailles",
"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 dobsidienne",
"description": "",
"source_reference": {
"page": "128"
}
},
{
"id": "Pouch of Insence",
"name": "Bourse dEncens",
"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"
}
} }
] ]
} }

View File

@@ -40,8 +40,8 @@
}, },
"Compendium": { "Compendium": {
"HideDisabledSources": { "HideDisabledSources": {
"Title": "[Compendium] Hide sources filter without reference", "Title": "[Compendium] Hide unavailable sources",
"Hint": "Hide empty source with no elements in source filter." "Hint": "Hide sources that have no available content from the source filter dropdown."
}, },
"HideEmptySourcesFromPlayers": { "HideEmptySourcesFromPlayers": {
"Title": "[Compendium] Hide elements with empty reference", "Title": "[Compendium] Hide elements with empty reference",
@@ -76,7 +76,10 @@
"signature_scroll": "Signature Scroll", "signature_scroll": "Signature Scroll",
"item_pattern": "Item Pattern", "item_pattern": "Item Pattern",
"army_fortification": "Fortification", "army_fortification": "Fortification",
"army_cohort": "Cohort" "army_cohort": "Cohort",
"arcane": "Arcane",
"etat": "State",
"mystere": "Mystery"
}, },
"Journal": { "Journal": {
"journal": "Journal" "journal": "Journal"
@@ -136,6 +139,7 @@
"player_filter_label": "Player filter", "player_filter_label": "Player filter",
"player_filter_tooltip": "Apply player filter", "player_filter_tooltip": "Apply player filter",
"already_in_filter": "Already in filter", "already_in_filter": "Already in filter",
"no_results": "Not Found",
"sources_categories": { "sources_categories": {
"rules": "Rules", "rules": "Rules",
"adventures": "Adventures", "adventures": "Adventures",
@@ -317,7 +321,8 @@
"school_ability": "School Ability", "school_ability": "School Ability",
"mastery_ability": "Mastery Ability", "mastery_ability": "Mastery Ability",
"title_ability": "Title Ability", "title_ability": "Title Ability",
"specificity": "Specificity" "specificity": "Specificity",
"mot_invocation": "Invocation Word"
}, },
"peculiarities": { "peculiarities": {
"types": { "types": {
@@ -492,7 +497,10 @@
"rarity_modifier": "Rarity modifier", "rarity_modifier": "Rarity modifier",
"item_pattern": "Item Patterns", "item_pattern": "Item Patterns",
"signature_scroll": "Signature Scrolls", "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_types": {
"character": "Player Character", "character": "Player Character",
@@ -705,6 +713,7 @@
"demeanor": { "demeanor": {
"adaptable": "Adaptable", "adaptable": "Adaptable",
"aggressive": "Aggressive", "aggressive": "Aggressive",
"alluring": "Alluring",
"ambitious": "Ambitious", "ambitious": "Ambitious",
"amiable": "Amiable", "amiable": "Amiable",
"analytical": "Analytical", "analytical": "Analytical",
@@ -713,6 +722,7 @@
"assertive": "Assertive", "assertive": "Assertive",
"beguiling": "Beguiling", "beguiling": "Beguiling",
"bitter": "Bitter", "bitter": "Bitter",
"bloodthirsty": "Bloodthirsty",
"bold": "Bold", "bold": "Bold",
"calculating": "Calculating", "calculating": "Calculating",
"calm": "Calm", "calm": "Calm",
@@ -723,37 +733,68 @@
"confused": "Confused", "confused": "Confused",
"courageous": "Courageous", "courageous": "Courageous",
"cowardly": "Cowardly", "cowardly": "Cowardly",
"crestfallen": "Crestfallen",
"curious": "Curious", "curious": "Curious",
"defensive": "Defensive",
"dependable": "Dependable", "dependable": "Dependable",
"detached": "Detached", "detached": "Detached",
"determined": "Determined",
"devoted": "Devoted",
"direct": "Direct",
"disheartened": "Disheartened", "disheartened": "Disheartened",
"dour": "Dour",
"duplicitous": "Duplicitous",
"effusive": "Effusive",
"enraged": "Enraged", "enraged": "Enraged",
"fanatical": "Fanatical",
"feral": "Feral", "feral": "Feral",
"fervent": "Fervent",
"fickle": "Fickle", "fickle": "Fickle",
"fierce": "Fierce", "fierce": "Fierce",
"flighty": "Flighty", "flighty": "Flighty",
"flippant": "Flippant", "flippant": "Flippant",
"friendly": "Friendly", "friendly": "Friendly",
"gruff": "Gruff", "gruff": "Gruff",
"honorable": "Honorable",
"hubristic": "Prétentieuse",
"hungry": "Hungry", "hungry": "Hungry",
"idealistic": "Idealistic",
"imposing": "Imposing",
"inquisitive": "Inquisitive",
"intense": "Intense", "intense": "Intense",
"intimidating": "Intimidating", "intimidating": "Intimidating",
"irritable": "Irritable", "irritable": "Irritable",
"loyal": "Loyal", "loyal": "Loyal",
"methodical": "Methodical",
"meticulous": "Meticulous",
"mischievous": "Mischievous", "mischievous": "Mischievous",
"moon_blessed": "Moon-blessed",
"morose": "Morose", "morose": "Morose",
"near_feral": "Near feral",
"nurturing": "Nurturing", "nurturing": "Nurturing",
"obsessed": "Obsessed",
"obstinate": "Obstinate", "obstinate": "Obstinate",
"opportunistic": "Opportunistic", "opportunistic": "Opportunistic",
"otherworldly": "Otherworldly",
"outgoing": "Outgoing",
"passionate": "Passionate", "passionate": "Passionate",
"patient": "Patient",
"personable": "Personable",
"playful": "Playful", "playful": "Playful",
"power_hungry": "Power hungry", "power_hungry": "Power hungry",
"proud": "Proud", "proud": "Proud",
"refined": "Refined",
"reserved": "Reserved",
"restrained": "Restrained", "restrained": "Restrained",
"righteous": "Righteous",
"scheming": "Scheming", "scheming": "Scheming",
"serene": "Serene", "serene": "Serene",
"serious": "Serious", "serious": "Serious",
"shrewd": "Shrewd", "shrewd": "Shrewd",
"sinister": "Sinister",
"sociable": "Sociable",
"stoic": "Stoic",
"starved": "Starved",
"stubborn": "Stubborn", "stubborn": "Stubborn",
"suspicious": "Suspicious", "suspicious": "Suspicious",
"teasing": "Teasing", "teasing": "Teasing",
@@ -761,7 +802,12 @@
"uncertain": "Uncertain", "uncertain": "Uncertain",
"unenthused": "Unenthused", "unenthused": "Unenthused",
"vain": "Vain", "vain": "Vain",
"wary": "Wary" "vengeful": "Vengeful",
"vindictive": "Vindictive",
"wary": "Wary",
"watchful": "Watchful",
"wrathful": "Wrathful",
"zealous": "Zealous"
}, },
"compendium": { "compendium": {
"filter_rank": "Show Rank", "filter_rank": "Show Rank",
@@ -769,14 +815,15 @@
"filter": { "filter": {
"rank": "Rank", "rank": "Rank",
"rarity": "Rarity", "rarity": "Rarity",
"ring": "Ring" "ring": "Ring",
"clear": "Clear Filter"
} }
}, },
"source_reference": { "source_reference": {
"core_rulebook": "Core Rulebook", "core_rulebook": "Core Rulebook",
"emerald_empire": "Emerald Empire", "emerald_empire": "Emerald Empire",
"shadowlands": "Shadowlands", "shadowlands": "Shadowlands",
"court_of_stones": "Court of Stones", "court_of_stones": "Courts of Stone",
"path_of_waves": "Path of Waves", "path_of_waves": "Path of Waves",
"celestial_realms": "Celestial Realms", "celestial_realms": "Celestial Realms",
"fields_of_victory": "Fields of Victory", "fields_of_victory": "Fields of Victory",
@@ -799,6 +846,169 @@
"the_scroll_or_the_blade": "The Scroll or the Blade", "the_scroll_or_the_blade": "The Scroll or the Blade",
"legacies_of_war": "Legacies of War", "legacies_of_war": "Legacies of War",
"children_of_the_five_winds": "Children of the Five Winds" "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}"
}
},
"chiaroscuro": {
"skill_ranks": {
"0": "—",
"initie": "Initiate",
"expert": "Expert",
"maitre": "Master",
"parangon1": "Paragon I",
"parangon2": "Paragon II",
"parangon3": "Paragon III"
},
"difficulties": {
"simple": "Simple (7)",
"moyenne": "Average (10)",
"assez_difficile": "Somewhat Difficult (13)",
"difficile": "Difficult (16)",
"tres_difficile": "Very Difficult (22)",
"heroique": "Heroic (28)",
"improbable": "Improbable (32)"
},
"aspects": {
"solar": "Solar Aspect",
"lunar": "Lunar Aspect",
"gauge": "Aspect Gauge",
"desequilibre_solaire": "Solar Imbalance",
"desequilibre_lunaire": "Lunar Imbalance"
},
"danger": {
"simple": "Simple",
"moyenne": "Average",
"assez_difficile": "Somewhat Difficult",
"difficile": "Difficult"
},
"arcane": {
"title": "Arcanes",
"label": "Arcane",
"arcane_type": "Type",
"application": "Skills",
"bonus": "Bonus",
"progression": "Progression",
"xp_cost": "XP Cost"
},
"etat": {
"title": "States",
"label": "State",
"application": "Application",
"mod": "Modifier",
"effect": "Effect",
"elimination": "Elimination Condition"
},
"mystere": {
"title": "Mysteries",
"label": "Mystery",
"mystere_type": "Type",
"mineur": "Minor",
"majeur": "Major",
"prerequisite_skill": "Prerequisite Skill",
"prerequisite_condition": "Prerequisite Condition"
},
"character": {
"is_samurai": "Samurai",
"quick_info": "Quick Info",
"default_ring": "Default Ring",
"region": "Region",
"education": "Education",
"past_problems": "Problematic Past",
"koku": "Koku",
"bu": "Bu",
"zeni": "Zeni"
},
"weapon": {
"bonus": "Bonus",
"categories": {
"arbalete": "Crossbow",
"arc": "Bow",
"contondante": "Bludgeoning Weapon",
"poing": "Hand Weapon",
"hast": "Polearm",
"improvisee": "Improvised Weapon",
"shinobi": "Shinobi Weapon",
"specialisee": "Specialized Weapon",
"bouclier": "Shield",
"hache": "Axe",
"naturel": "Natural",
"sabre": "Sword",
"nemuranai": "Nemuranai"
}
},
"armor": {
"categories": {
"vetement": "Clothing",
"leger": "Light",
"moyen": "Medium",
"lourd": "Heavy",
"nemuranai": "Nemuranai"
}
},
"item": {
"types": {
"ordinaire": "Ordinary",
"shinobi": "Shinobi",
"interdit": "Forbidden",
"gaijin": "Gaijin",
"nemuranai": "Nemuranai"
}
},
"technique": {
"mot_invocation": "Invocation Word",
"invocation_type": "Invocation Type",
"invocation_types": {
"general": "General",
"neutre": "Neutral",
"precis": "Precise"
},
"mode_invocation": "Invocation Mode"
},
"tabs": {
"invocations": "Invocations",
"identity": "Identity",
"identity_text1": "Appearance",
"identity_text2": "Biography"
},
"dice": {
"title": "Chiaroscuro Roll",
"difficulty_label": "Difficulty",
"modifier_label": "Modifier",
"options": "Options",
"aspect_point": "Aspect Point",
"assistance": "Assistance",
"total_dice": "Dice to roll",
"bonus": "Rank bonus",
"roll": "Roll",
"dice_result": "Dice total",
"adjusted": "Adjusted (Parangon)",
"success": "Success",
"failure": "Failure"
} }
} }
} }

View File

@@ -136,6 +136,7 @@
"player_filter_label": "Filtro de jugador", "player_filter_label": "Filtro de jugador",
"player_filter_tooltip": "Aplicar filtro de jugador", "player_filter_tooltip": "Aplicar filtro de jugador",
"already_in_filter": "Ya en el filtro", "already_in_filter": "Ya en el filtro",
"no_results": "Not Found",
"sources_categories": { "sources_categories": {
"rules": "Reglas", "rules": "Reglas",
"adventures": "Aventuras", "adventures": "Aventuras",
@@ -492,7 +493,10 @@
"rarity_modifier": "Modificador de rareza", "rarity_modifier": "Modificador de rareza",
"item_pattern": "Patrones de objetos", "item_pattern": "Patrones de objetos",
"signature_scroll": "Pergaminos espaciales", "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_types": {
"character": "Personaje jugador", "character": "Personaje jugador",
@@ -705,6 +709,7 @@
"demeanor": { "demeanor": {
"adaptable": "Adaptable", "adaptable": "Adaptable",
"aggressive": "Agresivo", "aggressive": "Agresivo",
"alluring": "Alluring",
"ambitious": "Ambicioso", "ambitious": "Ambicioso",
"amiable": "Amigable", "amiable": "Amigable",
"analytical": "Analítico", "analytical": "Analítico",
@@ -713,6 +718,7 @@
"assertive": "Firme", "assertive": "Firme",
"beguiling": "Seductor", "beguiling": "Seductor",
"bitter": "Amargado", "bitter": "Amargado",
"bloodthirsty": "Bloodthirsty",
"bold": "Atrevido", "bold": "Atrevido",
"calculating": "Calculador", "calculating": "Calculador",
"calm": "Calmado", "calm": "Calmado",
@@ -723,37 +729,68 @@
"confused": "Confuso", "confused": "Confuso",
"courageous": "Valiente", "courageous": "Valiente",
"cowardly": "Cobarde", "cowardly": "Cobarde",
"crestfallen": "Crestfallen",
"curious": "Curioso", "curious": "Curioso",
"defensive": "Defensive",
"dependable": "Fiable", "dependable": "Fiable",
"detached": "Desapegado", "detached": "Desapegado",
"determined": "Determined",
"devoted": "Devoted",
"direct": "Direct",
"disheartened": "Desanimado", "disheartened": "Desanimado",
"dour": "Dour",
"duplicitous": "Duplicitous",
"effusive": "Effusive",
"enraged": "Furioso", "enraged": "Furioso",
"fanatical": "Fanatical",
"feral": "Salvaje", "feral": "Salvaje",
"fervent": "Fervent",
"fickle": "Voluble", "fickle": "Voluble",
"fierce": "Fiero", "fierce": "Fiero",
"flighty": "Veleidoso", "flighty": "Veleidoso",
"flippant": "Frívolo", "flippant": "Frívolo",
"friendly": "Amable", "friendly": "Amable",
"gruff": "Hosco", "gruff": "Hosco",
"honorable": "Honorable",
"hubristic": "Hubristic",
"hungry": "Hambriento", "hungry": "Hambriento",
"idealistic": "Idealistic",
"imposing": "Imposing",
"inquisitive": "Inquisitive",
"intense": "Intenso", "intense": "Intenso",
"intimidating": "Intimidante", "intimidating": "Intimidante",
"irritable": "Irritable", "irritable": "Irritable",
"loyal": "Leal", "loyal": "Leal",
"methodical": "Methodical",
"meticulous": "Meticulous",
"mischievous": "Travieso", "mischievous": "Travieso",
"moon_blessed": "Moon-blessed",
"morose": "Taciturno", "morose": "Taciturno",
"near_feral": "Near feral",
"nurturing": "Animador", "nurturing": "Animador",
"obsessed": "Obsessed",
"obstinate": "Obstinado", "obstinate": "Obstinado",
"opportunistic": "Oportunista", "opportunistic": "Oportunista",
"otherworldly": "Otherworldly",
"outgoing": "Outgoing",
"passionate": "Apasionado", "passionate": "Apasionado",
"patient": "Patient",
"personable": "Personable",
"playful": "Juguetón", "playful": "Juguetón",
"power_hungry": "Ávido de poder", "power_hungry": "Ávido de poder",
"proud": "Orgulloso", "proud": "Orgulloso",
"refined": "Refined",
"reserved": "Reserved",
"restrained": "Contenido", "restrained": "Contenido",
"righteous": "Righteous",
"scheming": "Taimado", "scheming": "Taimado",
"serene": "Sereno", "serene": "Sereno",
"serious": "Serio", "serious": "Serio",
"shrewd": "Artero", "shrewd": "Artero",
"sinister": "Sinister",
"sociable": "Sociable",
"stoic": "Stoic",
"starved": "Starved",
"stubborn": "Testarudo", "stubborn": "Testarudo",
"suspicious": "Suspicaz", "suspicious": "Suspicaz",
"teasing": "Bromista", "teasing": "Bromista",
@@ -761,7 +798,12 @@
"uncertain": "Inseguro", "uncertain": "Inseguro",
"unenthused": "Sin entusiasmo", "unenthused": "Sin entusiasmo",
"vain": "Vanidoso", "vain": "Vanidoso",
"wary": "Precavido" "vengeful": "Vengeful",
"vindictive": "Vindictive",
"wary": "Precavido",
"watchful": "Watchful",
"wrathful": "Wrathful",
"zealous": "Zealous"
}, },
"compendium": { "compendium": {
"filter_rank": "Mostrar rango", "filter_rank": "Mostrar rango",
@@ -769,7 +811,8 @@
"filter": { "filter": {
"rank": "Rango", "rank": "Rango",
"rarity": "Rareza", "rarity": "Rareza",
"ring": "Anillo" "ring": "Anillo",
"clear": "Clear Filter"
} }
}, },
"source_reference": { "source_reference": {
@@ -799,6 +842,32 @@
"the_scroll_or_the_blade": "El pergamino o la espada", "the_scroll_or_the_blade": "El pergamino o la espada",
"legacies_of_war": "Legacies of War", "legacies_of_war": "Legacies of War",
"children_of_the_five_winds": "Children of the Five Winds" "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}"
} }
} }
} }

View File

@@ -76,7 +76,10 @@
"signature_scroll": "Rouleau de marque", "signature_scroll": "Rouleau de marque",
"item_pattern": "Procédé de fabrication", "item_pattern": "Procédé de fabrication",
"army_fortification": "Fortification", "army_fortification": "Fortification",
"army_cohort": "Régiment" "army_cohort": "Régiment",
"arcane": "Arcane",
"etat": "État",
"mystere": "Mystère"
}, },
"Journal": { "Journal": {
"journal": "Journal" "journal": "Journal"
@@ -136,6 +139,7 @@
"player_filter_label": "Filtre joueur", "player_filter_label": "Filtre joueur",
"player_filter_tooltip": "Applique le filtre des joueurs", "player_filter_tooltip": "Applique le filtre des joueurs",
"already_in_filter": "Filtre déjà appliqué", "already_in_filter": "Filtre déjà appliqué",
"no_results": "Aucun résultat",
"sources_categories": { "sources_categories": {
"rules": "Règles", "rules": "Règles",
"adventures": "Aventures", "adventures": "Aventures",
@@ -317,7 +321,8 @@
"school_ability": "Capacité d'école", "school_ability": "Capacité d'école",
"mastery_ability": "Capacité de maîtrise", "mastery_ability": "Capacité de maîtrise",
"title_ability": "Capacité de Titre", "title_ability": "Capacité de Titre",
"specificity": "Particularité" "specificity": "Particularité",
"mot_invocation": "Mot d'Invocation"
}, },
"peculiarities": { "peculiarities": {
"types": { "types": {
@@ -492,7 +497,10 @@
"rarity_modifier": "Modificateur de rareté", "rarity_modifier": "Modificateur de rareté",
"item_pattern": "Procédés de fabrication", "item_pattern": "Procédés de fabrication",
"signature_scroll": "Rouleaux de marque", "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_types": {
"character": "Personnage Joueur", "character": "Personnage Joueur",
@@ -703,65 +711,103 @@
"ujik": "Ujik" "ujik": "Ujik"
}, },
"demeanor": { "demeanor": {
"adaptable": "Adaptable", "adaptable": "Malléable",
"aggressive": "Agressive", "aggressive": "Agressive",
"alluring": "Attirante",
"ambitious": "Ambitieuse", "ambitious": "Ambitieuse",
"amiable": "Sympathique", "amiable": "Aimable",
"analytical": "Réfléchie", "analytical": "Analytique",
"angry": "Enervée", "angry": "En colère",
"arrogant": "Arrogante", "arrogant": "Arrogante",
"assertive": "Assurée", "assertive": "Sûre de soi",
"beguiling": "Séduisante", "beguiling": "Envoûtante",
"bitter": "Amère", "bitter": "Amère",
"bold": "Audacieuse", "bloodthirsty": "Sanguinaire",
"bold": "Courageuse",
"calculating": "Calculatrice", "calculating": "Calculatrice",
"calm": "Calme", "calm": "Calme",
"capricious": "Capricieuse", "capricious": "Capricieuse",
"cautious": "Prudente", "cautious": "Prudente",
"clever": "Astucieuse", "clever": "Malicieuse",
"compassionate": "Compatissante", "compassionate": "Compatissante",
"confused": "Confuse", "confused": "Confuse",
"courageous": "Courageuse", "courageous": "Courageuse",
"cowardly": "Lâche", "cowardly": "Lâche",
"crestfallen": "Démoralisée",
"curious": "Curieuse", "curious": "Curieuse",
"defensive": "Sur la défensive",
"dependable": "Fiable", "dependable": "Fiable",
"detached": "Détachée", "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", "enraged": "Enragée",
"fanatical": "Fanatique",
"feral": "Sauvage", "feral": "Sauvage",
"fickle": "Inconstante", "fervent": "Dévote",
"fickle": "Volatile",
"fierce": "Féroce", "fierce": "Féroce",
"flighty": "Volage", "flighty": "Inconstante",
"flippant": "Désinvolte", "flippant": "Désinvolte",
"friendly": "Amicale", "friendly": "Amicale",
"gruff": "Bourrue", "gruff": "Bourrue",
"honorable": "Honorable",
"hubristic": "Prétentieuse",
"hungry": "Affamée", "hungry": "Affamée",
"intense": "Intense", "idealistic": "Idéaliste",
"imposing": "Impressionnante",
"inquisitive": "Inquisitrice",
"intense": "Excessive",
"intimidating": "Intimidante", "intimidating": "Intimidante",
"irritable": "Irritable", "irritable": "Colérique",
"loyal": "Fidèle", "loyal": "Loyale",
"mischievous": "Malicieuse", "methodical": "Méthodique",
"meticulous": "Méticuleuse",
"mischievous": "Taquine",
"moon_blessed": "Bénie par la Lune",
"morose": "Morose", "morose": "Morose",
"nurturing": "Encourageante", "near_feral": "Presque sauvage",
"nurturing": "Maternelle",
"obsessed": "Obsessionnelle",
"obstinate": "Obstinée", "obstinate": "Obstinée",
"opportunistic": "Opportuniste", "opportunistic": "Opportuniste",
"otherworldly": "Mystique",
"outgoing": "Agréable",
"passionate": "Passionnée", "passionate": "Passionnée",
"playful": "Enjouée", "patient": "Patiente",
"personable": "Avenante",
"playful": "Joueuse",
"power_hungry": "Avide de pouvoir", "power_hungry": "Avide de pouvoir",
"proud": "Fière", "proud": "Fière",
"restrained": "Restreinte", "refined": "Raffinée",
"scheming": "Intrigante", "restrained": "Modérée",
"reserved": "Réservée",
"righteous": "Intègre",
"scheming": "Fourbe",
"serene": "Sereine", "serene": "Sereine",
"serious": "Sérieuse", "serious": "Sérieuse",
"shrewd": "Astucieuse", "shrewd": "Rusée",
"sinister": "Sinistre",
"sociable": "Affable",
"starved": "Famélique",
"stoic": "Stoïque",
"stubborn": "Têtue", "stubborn": "Têtue",
"suspicious": "Soupçonneuse", "suspicious": "Suspicieuse",
"teasing": "Taquine", "teasing": "Moqueuse",
"territorial": "Territoriale", "territorial": "Territoriale",
"uncertain": "Incertaine", "uncertain": "Peu sûre de soi",
"unenthused": "Peu enthousiaste", "unenthused": "Amorphe",
"vain": "Vaine", "vain": "Orgueilleuse",
"wary": "Méfiante" "vengeful": "Vengeuse",
"vindictive": "Vindicative",
"wary": "Méfiante",
"watchful": "Attentif",
"wrathful": "Furieuse",
"zealous": "Zélée"
}, },
"compendium": { "compendium": {
"filter_rank": "Aff. Rangs", "filter_rank": "Aff. Rangs",
@@ -769,7 +815,8 @@
"filter": { "filter": {
"rank": "Rang", "rank": "Rang",
"rarity": "Rareté", "rarity": "Rareté",
"ring": "Anneau" "ring": "Anneau",
"clear": "Suppr. les Filtres"
} }
}, },
"source_reference": { "source_reference": {
@@ -799,6 +846,169 @@
"the_scroll_or_the_blade": "Le Parchemin ou le Sabre", "the_scroll_or_the_blade": "Le Parchemin ou le Sabre",
"legacies_of_war": "Les Flambeaux de la Guerre", "legacies_of_war": "Les Flambeaux de la Guerre",
"children_of_the_five_winds": "Les Enfants des Cinq Vents" "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}"
}
},
"chiaroscuro": {
"skill_ranks": {
"0": "—",
"initie": "Initié",
"expert": "Expert",
"maitre": "Maître",
"parangon1": "Parangon I",
"parangon2": "Parangon II",
"parangon3": "Parangon III"
},
"difficulties": {
"simple": "Simple (7)",
"moyenne": "Moyenne (10)",
"assez_difficile": "Assez difficile (13)",
"difficile": "Difficile (16)",
"tres_difficile": "Très difficile (22)",
"heroique": "Héroïque (28)",
"improbable": "Improbable (32)"
},
"aspects": {
"solar": "Aspect Solaire",
"lunar": "Aspect Lunaire",
"gauge": "Jauge d'Aspect",
"desequilibre_solaire": "Déséquilibre Solaire",
"desequilibre_lunaire": "Déséquilibre Lunaire"
},
"danger": {
"simple": "Simple",
"moyenne": "Moyenne",
"assez_difficile": "Assez Difficile",
"difficile": "Difficile"
},
"arcane": {
"title": "Arcanes",
"label": "Arcane",
"arcane_type": "Type",
"application": "Compétences",
"bonus": "Bonus",
"progression": "Progression",
"xp_cost": "Coût XP"
},
"etat": {
"title": "États",
"label": "État",
"application": "Application",
"mod": "Modificateur",
"effect": "Effet",
"elimination": "Condition d'élimination"
},
"mystere": {
"title": "Mystères",
"label": "Mystère",
"mystere_type": "Type",
"mineur": "Mineur",
"majeur": "Majeur",
"prerequisite_skill": "Compétence prérequis",
"prerequisite_condition": "Condition prérequis"
},
"character": {
"is_samurai": "Samouraï",
"quick_info": "Info rapide",
"default_ring": "Anneau par défaut",
"region": "Région",
"education": "Éducation",
"past_problems": "Passé problématique",
"koku": "Koku",
"bu": "Bu",
"zeni": "Zeni"
},
"weapon": {
"bonus": "Bonus",
"categories": {
"arbalete": "Arbalète",
"arc": "Arc",
"contondante": "Arme Contondante",
"poing": "Arme de Poing",
"hast": "Arme Hast",
"improvisee": "Arme Improvisée",
"shinobi": "Arme Shinobi",
"specialisee": "Arme Spécialisée",
"bouclier": "Bouclier",
"hache": "Hache",
"naturel": "Naturel",
"sabre": "Sabre",
"nemuranai": "Nemuranai"
}
},
"armor": {
"categories": {
"vetement": "Vêtement",
"leger": "Léger",
"moyen": "Moyen",
"lourd": "Lourd",
"nemuranai": "Nemuranai"
}
},
"item": {
"types": {
"ordinaire": "Ordinaire",
"shinobi": "Shinobi",
"interdit": "Interdit",
"gaijin": "Gaijin",
"nemuranai": "Nemuranai"
}
},
"technique": {
"mot_invocation": "Mot d'Invocation",
"invocation_type": "Type d'invocation",
"invocation_types": {
"general": "Général",
"neutre": "Neutre",
"precis": "Précis"
},
"mode_invocation": "Mode d'Invocation"
},
"tabs": {
"invocations": "Invocations",
"identity": "Identité",
"identity_text1": "Apparence",
"identity_text2": "Biographie"
},
"dice": {
"title": "Jet Chiaroscuro",
"difficulty_label": "Difficulté",
"modifier_label": "Modificateur",
"options": "Options",
"aspect_point": "Point d'Aspect",
"assistance": "Assistance",
"total_dice": "Dés à lancer",
"bonus": "Bonus rang",
"roll": "Lancer les dés",
"dice_result": "Somme des dés",
"adjusted": "Ajusté (Parangon)",
"success": "Réussite",
"failure": "Échec"
} }
} }
} }

View File

@@ -136,6 +136,7 @@
"player_filter_label": "Player filter", "player_filter_label": "Player filter",
"player_filter_tooltip": "Apply player filter", "player_filter_tooltip": "Apply player filter",
"already_in_filter": "Already in filter", "already_in_filter": "Already in filter",
"no_results": "Not Found",
"sources_categories": { "sources_categories": {
"rules": "Rules", "rules": "Rules",
"adventures": "Adventures", "adventures": "Adventures",
@@ -492,7 +493,10 @@
"rarity_modifier": "Modificatore rarità", "rarity_modifier": "Modificatore rarità",
"item_pattern": "Item Patterns", "item_pattern": "Item Patterns",
"signature_scroll": "Signature Scrolls", "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_types": {
"character": "Personaggio giocante", "character": "Personaggio giocante",
@@ -705,6 +709,7 @@
"demeanor": { "demeanor": {
"adaptable": "Flessibile", "adaptable": "Flessibile",
"aggressive": "Aggressivo", "aggressive": "Aggressivo",
"alluring": "Alluring",
"ambitious": "Ambizioso", "ambitious": "Ambizioso",
"amiable": "Affabile", "amiable": "Affabile",
"analytical": "Analitico", "analytical": "Analitico",
@@ -713,6 +718,7 @@
"assertive": "Risoluto", "assertive": "Risoluto",
"beguiling": "Ammaliante", "beguiling": "Ammaliante",
"bitter": "Amaro", "bitter": "Amaro",
"bloodthirsty": "Bloodthirsty",
"bold": "Ardito", "bold": "Ardito",
"calculating": "Calcolatore", "calculating": "Calcolatore",
"calm": "Calmo", "calm": "Calmo",
@@ -723,37 +729,68 @@
"confused": "Confuso", "confused": "Confuso",
"courageous": "Coraggioso", "courageous": "Coraggioso",
"cowardly": "Codardo", "cowardly": "Codardo",
"crestfallen": "Crestfallen",
"curious": "Curioso", "curious": "Curioso",
"defensive": "Defensive",
"dependable": "Affidabile", "dependable": "Affidabile",
"detached": "Distaccato", "detached": "Distaccato",
"determined": "Determined",
"devoted": "Devoted",
"direct": "Direct",
"disheartened": "Sconfortato", "disheartened": "Sconfortato",
"dour": "Dour",
"duplicitous": "Duplicitous",
"effusive": "Effusive",
"enraged": "Infuriato", "enraged": "Infuriato",
"fanatical": "Fanatical",
"feral": "Selvaggio", "feral": "Selvaggio",
"fervent": "Fervent",
"fickle": "Volubile", "fickle": "Volubile",
"fierce": "Agguerrito", "fierce": "Agguerrito",
"flighty": "Volubile", "flighty": "Volubile",
"flippant": "Irriverente", "flippant": "Irriverente",
"friendly": "Amichevole", "friendly": "Amichevole",
"gruff": "Burbero", "gruff": "Burbero",
"honorable": "Honorable",
"hubristic": "Prétentieuse",
"hungry": "Affamato", "hungry": "Affamato",
"idealistic": "Idealistic",
"imposing": "Imposing",
"inquisitive": "Inquisitive",
"intense": "Intenso", "intense": "Intenso",
"intimidating": "Intimidatorio", "intimidating": "Intimidatorio",
"irritable": "Irritabile", "irritable": "Irritabile",
"loyal": "Leale", "loyal": "Leale",
"methodical": "Methodical",
"meticulous": "Meticulous",
"mischievous": "Malandrino", "mischievous": "Malandrino",
"moon_blessed": "Moon-blessed",
"morose": "Cupo", "morose": "Cupo",
"near_feral": "Near feral",
"nurturing": "Materno", "nurturing": "Materno",
"obsessed": "Obsessed",
"obstinate": "Ostinato", "obstinate": "Ostinato",
"opportunistic": "Opportunista", "opportunistic": "Opportunista",
"otherworldly": "Otherworldly",
"outgoing": "Outgoing",
"passionate": "Appassionato", "passionate": "Appassionato",
"patient": "Patient",
"personable": "Personable",
"playful": "Giocoso", "playful": "Giocoso",
"power_hungry": "Affamato di potere", "power_hungry": "Affamato di potere",
"proud": "Orgoglioso", "proud": "Orgoglioso",
"refined": "Refined",
"reserved": "Reserved",
"restrained": "Contenuto", "restrained": "Contenuto",
"righteous": "Righteous",
"scheming": "Cospiratore", "scheming": "Cospiratore",
"serene": "Sereno", "serene": "Sereno",
"serious": "Serio", "serious": "Serio",
"shrewd": "Scaltro", "shrewd": "Scaltro",
"sinister": "Sinister",
"sociable": "Sociable",
"stoic": "Stoic",
"starved": "Starved",
"stubborn": "Testardo", "stubborn": "Testardo",
"suspicious": "Sospettoso", "suspicious": "Sospettoso",
"teasing": "Stuzzicante", "teasing": "Stuzzicante",
@@ -761,7 +798,12 @@
"uncertain": "Incerto", "uncertain": "Incerto",
"unenthused": "Non entusiasta", "unenthused": "Non entusiasta",
"vain": "Vanesio", "vain": "Vanesio",
"wary": "Diffidente" "vengeful": "Vengeful",
"vindictive": "Vindictive",
"wary": "Diffidente",
"watchful": "Watchful",
"wrathful": "Wrathful",
"zealous": "Zealous"
}, },
"compendium": { "compendium": {
"filter_rank": "Show Rank", "filter_rank": "Show Rank",
@@ -769,14 +811,15 @@
"filter": { "filter": {
"rank": "Rank", "rank": "Rank",
"rarity": "Rarity", "rarity": "Rarity",
"ring": "Ring" "ring": "Ring",
"clear": "Clear Filter"
} }
}, },
"source_reference": { "source_reference": {
"core_rulebook": "Core Rulebook", "core_rulebook": "Core Rulebook",
"emerald_empire": "Emerald Empire", "emerald_empire": "Emerald Empire",
"shadowlands": "Shadowlands", "shadowlands": "Shadowlands",
"court_of_stones": "Court of Stones", "court_of_stones": "Courts of Stone",
"path_of_waves": "Path of Waves", "path_of_waves": "Path of Waves",
"celestial_realms": "Celestial Realms", "celestial_realms": "Celestial Realms",
"fields_of_victory": "Fields of Victory", "fields_of_victory": "Fields of Victory",
@@ -799,6 +842,32 @@
"the_scroll_or_the_blade": "The Scroll or the Blade", "the_scroll_or_the_blade": "The Scroll or the Blade",
"legacies_of_war": "Legacies of War", "legacies_of_war": "Legacies of War",
"children_of_the_five_winds": "Children of the Five Winds" "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}"
} }
} }
} }

View File

@@ -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":"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":"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":"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":"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":"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":[]} {"_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":"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":"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":"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":[]}

View File

@@ -624,6 +624,36 @@ export class BaseCharacterSheetL5r5e extends BaseSheetL5r5e {
}); });
break; 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: default:
console.warn("L5R5E | BCS | Unsupported type", type); console.warn("L5R5E | BCS | Unsupported type", type);
break; break;

View File

@@ -56,11 +56,60 @@ export class CharacterSheetL5r5e extends BaseCharacterSheetL5r5e {
// Split Others advancements, and calculate xp spent and add it to total // Split Others advancements, and calculate xp spent and add it to total
this._prepareOthersAdvancement(sheetData); this._prepareOthersAdvancement(sheetData);
// Update spent_xp to actor
this.actor.system.xp_spent = sheetData.data.system.xp_spent;
// Total // Total
sheetData.data.system.xp_saved = Math.floor( sheetData.data.system.xp_saved = Math.floor(
parseInt(sheetData.data.system.xp_total) - parseInt(sheetData.data.system.xp_spent) parseInt(sheetData.data.system.xp_total) - parseInt(sheetData.data.system.xp_spent)
); );
// Chiaroscuro: Skill ranks list for <select>
sheetData.data.skillRanksList = Object.keys(CONFIG.l5r5e.skillRanks).map((id) => ({
id,
label: game.i18n.localize(`chiaroscuro.skill_ranks.${id}`),
}));
// Chiaroscuro: Normalize skill values 0 (number) → "0" (string) for selectOptions matching
for (const category of Object.values(sheetData.data.system.skills)) {
for (const [key, value] of Object.entries(category)) {
if (value === 0) category[key] = "0";
}
}
// Chiaroscuro: Aspects gauge data
const aspectsData = sheetData.data.system.aspects?.aspects ?? {};
const gauge = aspectsData.gauge ?? 0;
sheetData.data.aspectsData = {
solar: aspectsData.solar ?? 0,
lunar: aspectsData.lunar ?? 0,
gauge,
gaugePercent: ((gauge + 10) / 20) * 100,
gaugeColor: gauge > 0 ? "#d4a855" : gauge < 0 ? "#5588aa" : "#888888",
};
// Chiaroscuro: État items active on the character
sheetData.data.etatItems = sheetData.items.filter((i) => i.type === "etat");
// Chiaroscuro: Invocations split by type (from splitTechniquesList)
const invocations = sheetData.data.splitTechniquesList["mot_invocation"] ?? [];
sheetData.data.splitInvocationsList = {
general: invocations.filter((t) => !t.system.invocation_type || t.system.invocation_type === "general"),
neutre: invocations.filter((t) => t.system.invocation_type === "neutre"),
precis: invocations.filter((t) => t.system.invocation_type === "precis"),
};
// Chiaroscuro: Arcane items
sheetData.data.arcaneItems = sheetData.items.filter((i) => i.type === "arcane");
// Chiaroscuro: Identity tabs enriched HTML
sheetData.data.enrichedHtml.identity_text1 = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
this.actor.system.identity_text1 ?? "", { async: true }
);
sheetData.data.enrichedHtml.identity_text2 = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
this.actor.system.identity_text2 ?? "", { async: true }
);
return sheetData; return sheetData;
} }
@@ -114,6 +163,17 @@ export class CharacterSheetL5r5e extends BaseCharacterSheetL5r5e {
// Money +/- // Money +/-
html.find(".money-control").on("click", this._modifyMoney.bind(this)); html.find(".money-control").on("click", this._modifyMoney.bind(this));
// XP +/-
html.find(".xp-control").on("click", this._modifyXP.bind(this));
// Chiaroscuro: set default ring on ring-name click
html.find(".ring-set-default").on("click", (event) => {
event.preventDefault();
const ring = $(event.currentTarget).data("ring");
this.actor.update({ "system.default_ring": ring });
});
// Advancements Tab to current rank onload // Advancements Tab to current rank onload
// TODO class "Active" Bug on load, dunno why :/ // TODO class "Active" Bug on load, dunno why :/
this._tabs this._tabs
@@ -121,6 +181,18 @@ export class CharacterSheetL5r5e extends BaseCharacterSheetL5r5e {
.activate("advancement_rank_" + (this.actor.system.identity.school_rank || 0)); .activate("advancement_rank_" + (this.actor.system.identity.school_rank || 0));
} }
/**
* Override base dice picker to open Chiaroscuro d6 dialog.
* @param {Event} event
*/
_openDicePickerForSkill(event) {
event.preventDefault();
const el = $(event.currentTarget);
const skillId = el.data("skill");
const ringId = el.data("ring") || this.actor.system?.default_ring || "void";
new game.l5r5e.ChiaroscuroDiceDialog({ actor: this.actor, ringId, skillId }).render(true);
}
/** /**
* Split the school advancement, calculate the total xp spent and the current total xp spent by rank * Split the school advancement, calculate the total xp spent and the current total xp spent by rank
*/ */
@@ -149,6 +221,12 @@ export class CharacterSheetL5r5e extends BaseCharacterSheetL5r5e {
adv[rank].spent.total += xp_used_total; adv[rank].spent.total += xp_used_total;
adv[rank].spent.curriculum += xp_used; 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; sheetData.data.advancementsListByRank = adv;
} }
@@ -206,6 +284,13 @@ export class CharacterSheetL5r5e extends BaseCharacterSheetL5r5e {
delete formData["system.money.zeni"]; delete formData["system.money.zeni"];
} }
// Chiaroscuro: convert skill rank "0" (string from <select>) back to 0 (number)
for (const [key, value] of Object.entries(formData)) {
if (key.startsWith("system.skills.") && value === "0") {
formData[key] = 0;
}
}
// Save computed values // Save computed values
const currentData = this.object.system; const currentData = this.object.system;
formData["system.focus"] = currentData.focus; formData["system.focus"] = currentData.focus;
@@ -285,6 +370,35 @@ export class CharacterSheetL5r5e extends BaseCharacterSheetL5r5e {
this.render(false); 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 * Add +1 to actor school rank
* @param {Event} event * @param {Event} event

View File

@@ -50,6 +50,21 @@ export class NpcSheetL5r5e extends BaseCharacterSheetL5r5e {
label: game.i18n.localize("l5r5e.character_types." + e), label: game.i18n.localize("l5r5e.character_types." + e),
})); }));
// Danger levels for martial/social danger selects
const dangerLevels = ["simple", "moyenne", "assez_difficile", "difficile"];
sheetData.data.dangerList = dangerLevels.map((id) => ({
id,
label: game.i18n.localize(`chiaroscuro.danger.${id}`),
}));
// Invocations list (mot_invocation techniques, split by type like character sheet)
const invocations = sheetData.data.splitTechniquesList?.["mot_invocation"] ?? [];
sheetData.data.splitInvocationsList = {
general: invocations.filter((t) => !t.system.invocation_type || t.system.invocation_type === "general"),
neutre: invocations.filter((t) => t.system.invocation_type === "neutre"),
precis: invocations.filter((t) => t.system.invocation_type === "precis"),
};
return sheetData; return sheetData;
} }
@@ -101,4 +116,16 @@ export class NpcSheetL5r5e extends BaseCharacterSheetL5r5e {
return super._updateObject(event, formData); return super._updateObject(event, formData);
} }
/**
* Override base dice picker to open Chiaroscuro d6 dialog.
* @param {Event} event
*/
_openDicePickerForSkill(event) {
event.preventDefault();
const el = $(event.currentTarget);
const skillId = el.data("skill");
const ringId = el.data("ring") || this.actor.system?.default_ring || "void";
new game.l5r5e.ChiaroscuroDiceDialog({ actor: this.actor, ringId, skillId }).render(true);
}
} }

View 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;
}
}

View 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
}
}
}

View File

@@ -14,6 +14,50 @@ export const L5R5E = {
skillCostMultiplier: 2, skillCostMultiplier: 2,
techniqueCost: 3, techniqueCost: 3,
}, },
// --- Chiaroscuro additions ---
/** Skill rank enum values and associated flat bonus */
skillRanks: {
0: { bonus: 0 },
initie: { bonus: 1 },
expert: { bonus: 2 },
maitre: { bonus: 3 },
parangon1: { bonus: 3, passive: 1 }, // dice results of 1 count as 2
parangon2: { bonus: 3, passive: 2 }, // dice results of 1-2 count as 3
parangon3: { bonus: 3, passive: 3 }, // dice results of 1-3 count as 4
},
/** Difficulty thresholds (Chiaroscuro scale) */
difficulties: {
simple: 7,
moyenne: 10,
assez_difficile: 13,
difficile: 16,
tres_difficile: 22,
heroique: 28,
improbable: 32,
},
/** Ring colors for Chiaroscuro visual style */
ringColors: {
air: "rgb(145, 120, 150)",
water: "rgb(95, 145, 155)",
fire: "rgb(155, 115, 80)",
earth: "rgb(105, 150, 120)",
void: "rgb(75, 70, 65)",
},
/** Aspect gauge configuration */
aspects: {
solarConditionId: "desequilibre_lunaire",
lunarConditionId: "desequilibre_solaire",
imbalanceThreshold: 5,
resetThreshold: 10,
},
// --- End Chiaroscuro additions ---
// For rings wound to be aligned, add them first // For rings wound to be aligned, add them first
conditions: [{ conditions: [{
id: "lightly_wounded_fire", id: "lightly_wounded_fire",
@@ -180,6 +224,16 @@ export const L5R5E = {
name: "l5r5e.conditions.unconscious", name: "l5r5e.conditions.unconscious",
img: "systems/l5r5e/assets/icons/conditions/unconscious.webp", img: "systems/l5r5e/assets/icons/conditions/unconscious.webp",
system: { id: "L5RCoreCon000015" } system: { id: "L5RCoreCon000015" }
},{
id: "desequilibre_solaire",
name: "chiaroscuro.aspects.desequilibre_solaire",
img: "systems/l5r5e/assets/icons/conditions/desequilibre_solaire.webp",
system: { id: "ChiaCon000001" }
},{
id: "desequilibre_lunaire",
name: "chiaroscuro.aspects.desequilibre_lunaire",
img: "systems/l5r5e/assets/icons/conditions/desequilibre_lunaire.webp",
system: { id: "ChiaCon000002" }
}], }],
regex: { regex: {
techniqueDifficulty: /^@([TS]):([^|]+?)(?:\|(min|max)(?:\(([^)]+?)\))?)?$/, techniqueDifficulty: /^@([TS]):([^|]+?)(?:\|(min|max)(?:\(([^)]+?)\))?)?$/,
@@ -344,6 +398,8 @@ L5R5E.techniques.set("mastery_ability", { type: "school", displayInTypes: false
L5R5E.techniques.set("title_ability", { type: "title", displayInTypes: false }); L5R5E.techniques.set("title_ability", { type: "title", displayInTypes: false });
// Custom // Custom
L5R5E.techniques.set("specificity", { type: "custom", displayInTypes: false }); L5R5E.techniques.set("specificity", { type: "custom", displayInTypes: false });
// Chiaroscuro
L5R5E.techniques.set("mot_invocation", { type: "chiaroscuro", displayInTypes: true });
// *** SkillId - CategoryId *** // *** SkillId - CategoryId ***
L5R5E.skills = new Map(); L5R5E.skills = new Map();
@@ -456,6 +512,7 @@ L5R5E.demeanors = [
{ id: "adaptable", mod: { water: 2, earth: -2 } }, { id: "adaptable", mod: { water: 2, earth: -2 } },
{ id: "aggressive", mod: { fire: 2, air: -2 } }, { id: "aggressive", mod: { fire: 2, air: -2 } },
{ id: "aggressive", mod: { fire: 2, water: -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: "ambitious", mod: { fire: 2, water: -2 } },
{ id: "amiable", mod: { air: 2, earth: -2 } }, { id: "amiable", mod: { air: 2, earth: -2 } },
{ id: "analytical", mod: { fire: 2, air: -2 } }, { id: "analytical", mod: { fire: 2, air: -2 } },
@@ -466,23 +523,38 @@ L5R5E.demeanors = [
{ id: "beguiling", mod: { air: 2, earth: -2 } }, { id: "beguiling", mod: { air: 2, earth: -2 } },
{ id: "beguiling", mod: { fire: 2, earth: -2 } }, { id: "beguiling", mod: { fire: 2, earth: -2 } },
{ id: "bitter", mod: { fire: 2, air: -2 } }, { id: "bitter", mod: { fire: 2, air: -2 } },
{ id: "bloodthirsty", mod: { fire: 2, water: -2 } },
{ id: "bold", mod: { fire: 1, earth: -1 } }, { id: "bold", mod: { fire: 1, earth: -1 } },
{ id: "calculating", mod: { air: 2, fire: -2 } }, { id: "calculating", mod: { air: 2, fire: -2 } },
{ id: "calm", mod: { fire: 2, air: -2 } }, { id: "calm", mod: { fire: 2, air: -2 } },
{ id: "capricious", mod: { air: 2, earth: -2 } }, { id: "capricious", mod: { air: 2, earth: -2 } },
{ id: "cautious", 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: "clever", mod: { air: 2, earth: -2 } },
{ id: "compassionate", mod: { fire: 2, air: -1, water: -1}}, { 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: "confused", mod: { fire: 1, void: 1, air: -2 } },
{ id: "courageous", mod: { air: 2, earth: -2 } }, { id: "courageous", mod: { air: 2, earth: -2 } },
{ id: "cowardly", mod: { earth: 2, fire: -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: { earth: 1, void: -2 } },
{ id: "curious", mod: { fire: 1, void: 1, air: -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: "dependable", mod: { fire: 1, water: 1, earth: -2 } },
{ id: "detached", mod: { earth: 1, fire: 1, void: -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: "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: "enraged", mod: { air: 1, fire: -2 } },
{ id: "fanatical", mod: { earth: 1, air: 1, fire: -2 } },
{ id: "feral", mod: { air: 2, 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: "fickle", mod: { fire: 2, air: -2 } },
{ id: "fierce", mod: { fire: 2, earth: -2 } }, { id: "fierce", mod: { fire: 2, earth: -2 } },
{ id: "flighty", mod: { air: 2, fire: -2 } }, { id: "flighty", mod: { air: 2, fire: -2 } },
@@ -490,32 +562,55 @@ L5R5E.demeanors = [
{ id: "flippant", mod: { fire: 2, air: -2 } }, { id: "flippant", mod: { fire: 2, air: -2 } },
{ id: "friendly", mod: { fire: 1, earth: -2, water: -2 } }, { id: "friendly", mod: { fire: 1, earth: -2, water: -2 } },
{ id: "gruff", mod: { water: 2, earth: -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: "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: { air: 2, water: -2 } },
{ id: "intense", mod: { fire: 2, water: -2 } }, { id: "intense", mod: { fire: 2, water: -2 } },
{ id: "intimidating", mod: { fire: 2, air: -2 } }, { id: "intimidating", mod: { fire: 2, air: -2 } },
{ id: "irritable", mod: { fire: 2, air: -1, water: -1 } }, { id: "irritable", mod: { fire: 2, air: -1, water: -1 } },
{ id: "loyal", mod: { air: 1, earth: -2, fire: -2 } }, { id: "loyal", mod: { air: 1, earth: -2, fire: -2 } },
{ id: "loyal", mod: { water: 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: { fire: 2, air: -2 } },
{ id: "mischievous", mod: { air: 2, earth: -2 } }, { id: "mischievous", mod: { air: 2, earth: -2 } },
{ id: "mischievous", mod: { earth: 2, fire: -2 } }, { id: "mischievous", mod: { earth: 2, fire: -2 } },
{ id: "moon_blessed", mod: { water: 2, fire: -2 } },
{ id: "morose", 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: "nurturing", mod: { earth: 2, fire: -2 } },
{ id: "obsessed", mod: { earth: 2, air: -2 } },
{ id: "obstinate", mod: { earth: 2, air: -2 } }, { id: "obstinate", mod: { earth: 2, air: -2 } },
{ id: "obstinate", mod: { water: 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: "opportunistic", mod: { water: 2, fire: -2 } },
{ id: "passionate", mod: { earth: 2, air: -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: { earth: 2, water: -2 } },
{ id: "playful", mod: { fire: 1, air: 1, void: -2 } }, { id: "playful", mod: { fire: 1, air: 1, void: -2 } },
{ id: "power_hungry", mod: { fire: 2, earth: -2 } }, { id: "power_hungry", mod: { fire: 2, earth: -2 } },
{ id: "proud", 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: "restrained", mod: { earth: 2, air: -2 } },
{ id: "righteous", mod: { water: 2, fire: -1, void: -1 } },
{ id: "scheming", mod: { air: 2, void: -2 } }, { id: "scheming", mod: { air: 2, void: -2 } },
{ id: "serene", mod: { fire: 2, void: -2 } }, { id: "serene", mod: { fire: 2, void: -2 } },
{ id: "serene", mod: { void: 2, fire: -2 } }, { id: "serene", mod: { void: 2, fire: -2 } },
{ id: "serious", mod: { fire: 2, earth: -2 } }, { id: "serious", mod: { fire: 2, earth: -2 } },
{ id: "shrewd", mod: { air: 2, fire: -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: "stubborn", mod: { earth: 2, water: -2 } },
{ id: "suspicious", mod: { air: 2, earth: -2 } }, { id: "suspicious", mod: { air: 2, earth: -2 } },
{ id: "teasing", mod: { air: 2, earth: -2 } }, { id: "teasing", mod: { air: 2, earth: -2 } },
@@ -523,5 +618,10 @@ L5R5E.demeanors = [
{ id: "uncertain", mod: { air: 2, fire: -2 } }, { id: "uncertain", mod: { air: 2, fire: -2 } },
{ id: "unenthused", mod: { earth: 2, fire: -2 } }, { id: "unenthused", mod: { earth: 2, fire: -2 } },
{ id: "vain", mod: { earth: 2, air: -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: "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 } },
]; ];

View File

@@ -1,47 +1,110 @@
import { L5r5eHtmlMultiSelectElement } from "../misc/l5r5e-multiselect.js"; import { L5r5eHtmlMultiSelectElement } from "../misc/l5r5e-multiselect.js";
/** /**
* A subclass of [ArrayField]{@link ArrayField} which supports a set of contained elements. * A Foundry `SetField` that renders as an {@link L5r5eHtmlMultiSelectElement} chip-input.
* Elements in this set are treated as fungible and may be represented in any order or discarded if invalid. *
* 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 { export class L5r5eSetField extends foundry.data.fields.SetField {
/**
* Saved constructor options, used to reconstruct the multiselect input on form render.
* @type {object}
*/
#savedOptions;
// We don't get the options we expect when we convert this to input, /**
// So store them here * @param {object} options
#savedOptions; * @param {object} context
*/
constructor(options = {}, context = {}) {
super(
new foundry.data.fields.StringField({
choices: options.options?.map((option) => option.value) ?? [],
}),
options,
context
);
constructor(options={}, context={}) { this.#savedOptions = options;
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));
} }
/** @override */ /**
* @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();
}
return new Set(super.initialize(value, model, options).filter(Boolean));
}
/**
* @param {Set} value
* @return {*[]|*}
* @override
*/
toObject(value) { toObject(value) {
if ( !value ) return value; if (!value) {
return Array.from(value).map(v => this.element.toObject(v)); 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) { _toInput(config) {
const e = this.element; return L5r5eHtmlMultiSelectElement.create({
return L5r5eHtmlMultiSelectElement.create({ name: config.name,
name: config.name, options: this.#savedOptions.options,
options: this.#savedOptions.options, groups: this.#savedOptions.groups,
groups: this.#savedOptions.groups, value: config.value,
value: config.value, localize: config.localize,
localize: config.localize hideDisabledOptions: this.#savedOptions.hideDisabledOptions,
}); });
} }
} }

View File

@@ -0,0 +1,327 @@
/**
* Chiaroscuro Dice Dialog
*
* d6 pool system: ring value × multiplier d6, sum vs difficulty.
* Multiplier: ×1 base, ×2 if aspect or assistance, ×3 if both.
* Parangon passives adjust individual die results before summing.
*/
export class ChiaroscuroDiceDialog extends FormApplication {
/**
* Current Actor
* @type {ActorL5r5e}
* @private
*/
_actor = null;
/**
* Payload Object
*/
object = {
ring: { id: "void", value: 1 },
skill: { id: "", name: "", bonus: 0, rank: "0" },
difficulty: { id: "moyenne", value: 10 },
modifier: 0,
useAspectPoint: false,
aspectType: "solar",
useAssistance: false,
};
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: "l5r5e-chiaroscuro-dice-dialog",
classes: ["l5r5e", "chiaroscuro-dice-dialog"],
template: CONFIG.l5r5e.paths.templates + "dice/chiaroscuro-dice-dialog.html",
title: game.i18n.localize("chiaroscuro.dice.title"),
width: 440,
height: "auto",
});
}
get id() {
return `l5r5e-chiaroscuro-dice-dialog-${this._actor?.id ?? "no-actor"}`;
}
get title() {
return game.i18n.localize("chiaroscuro.dice.title") + (this._actor ? " — " + this._actor.name : "");
}
/**
* Total dice to roll (ring value × multiplier)
* @return {number}
*/
get totalDice() {
const base = this.object.ring.value;
const both = this.object.useAspectPoint && this.object.useAssistance;
const either = this.object.useAspectPoint || this.object.useAssistance;
return base * (both ? 3 : either ? 2 : 1);
}
/**
* @param options actor, actorId, ringId, skillId
*/
constructor(options = {}) {
super({}, options);
// Resolve actor
[
options?.actor,
game.actors.get(options?.actorId),
canvas.tokens.controlled[0]?.actor,
game.user.character,
].forEach((actor) => {
if (!this._actor && actor instanceof Actor && actor.isOwner) {
this._actor = actor;
}
});
// Default ring: options > actor default_ring > void
const ringId = options.ringId ?? this._actor?.system?.default_ring ?? "void";
this.ringId = ringId;
// Skill
if (options.skillId) {
this.skillId = options.skillId;
}
}
/**
* Set ring (id + value from actor)
* @param {string} ringId
*/
set ringId(ringId) {
this.object.ring.id = CONFIG.l5r5e.stances.includes(ringId) ? ringId : "void";
this.object.ring.value = this._actor?.system?.rings?.[this.object.ring.id] || 1;
// Auto-derive aspect type from ring (fire/earth → solar, air/water → lunar; void = manual)
if (this.object.ring.id !== "void") {
this.object.aspectType = ["fire", "earth"].includes(this.object.ring.id) ? "solar" : "lunar";
}
}
/**
* Set skill (id, name, rank, bonus)
* @param {string} skillId
*/
set skillId(skillId) {
if (!skillId) return;
const catId = CONFIG.l5r5e.skills.get(skillId.toLowerCase().trim());
const rank = this._actor?.system?.skills?.[catId]?.[skillId] ?? "0";
this.object.skill = {
...this.object.skill,
id: skillId,
name: catId ? game.i18n.localize(`l5r5e.skills.${catId}.${skillId}`) : skillId,
rank,
bonus: CONFIG.l5r5e.skillRanks?.[rank]?.bonus ?? 0,
};
}
async getData(options = null) {
const difficultiesList = Object.entries(CONFIG.l5r5e.difficulties).map(([id, value]) => ({
id,
label: game.i18n.localize(`chiaroscuro.difficulties.${id}`),
value,
}));
const aspectsList = [
{ id: "solar", label: game.i18n.localize("chiaroscuro.aspects.solar") },
{ id: "lunar", label: game.i18n.localize("chiaroscuro.aspects.lunar") },
];
return {
...(await super.getData(options)),
actor: this._actor,
data: this.object,
totalDice: this.totalDice,
ringsList: game.l5r5e.HelpersL5r5e.getRingsList(this._actor),
difficultiesList,
aspectsList,
isVoidRing: this.object.ring.id === "void",
quickInfo: this._actor?.system?.quick_info ?? "",
};
}
activateListeners(html) {
super.activateListeners(html);
// Ring selector
html.find(".ring-selection-chi").on("click", async (event) => {
event.preventDefault();
event.stopPropagation();
this.ringId = event.currentTarget.dataset.ringid;
this.render(false);
});
// Difficulty select
html.find("select[name='difficulty.id']").on("change", (event) => {
this.object.difficulty.id = event.target.value;
this.object.difficulty.value = CONFIG.l5r5e.difficulties[this.object.difficulty.id];
this.render(false);
});
// Flat modifier
html.find("input[name='modifier']").on("change", (event) => {
this.object.modifier = parseInt(event.target.value) || 0;
});
// Aspect point checkbox
html.find("#use_aspect_point").on("change", (event) => {
this.object.useAspectPoint = event.target.checked;
this.render(false);
});
// Aspect type select (solar / lunar)
html.find("select[name='aspectType']").on("change", (event) => {
this.object.aspectType = event.target.value;
});
// Assistance checkbox
html.find("#use_assistance").on("change", (event) => {
this.object.useAssistance = event.target.checked;
this.render(false);
});
}
async _updateObject(event, formData) {
const nbDice = this.totalDice;
const skillRank = this.object.skill.rank;
const skillBonus = this.object.skill.bonus;
const flatModifier = this.object.modifier;
const difficulty = this.object.difficulty.value;
// Roll the dice using FoundryVTT Roll API
const roll = await new Roll(`${nbDice}d6`).evaluate();
const rawResults = roll.dice[0].results.map((r) => r.result);
// Apply parangon passive adjustments
const adjustedResults = rawResults.map((r) => this._applyParangon(r, skillRank));
const diceAdjustedFlags = rawResults.map((r, i) => adjustedResults[i] !== r);
const wasAdjusted = diceAdjustedFlags.some(Boolean);
// Compute total
const rawSum = adjustedResults.reduce((a, b) => a + b, 0);
const total = rawSum + skillBonus + flatModifier;
const success = total >= difficulty;
const bonus = success ? total - difficulty : 0;
// Update aspect gauge after roll
if (this._actor && this.object.useAspectPoint) {
await this._updateAspectGauge();
}
// Post chat message
await this._sendChatMessage({
nbDice,
rawResults,
adjustedResults,
diceAdjustedFlags,
wasAdjusted,
rawSum,
total,
skillBonus,
flatModifier,
difficulty,
success,
bonus,
});
return this.close();
}
/**
* Apply parangon rank passive: replace low die results with higher value.
* parangon1: 1 → 2
* parangon2: 12 → 3
* parangon3: 13 → 4
* @param {number} result
* @param {string} rank
* @return {number}
*/
_applyParangon(result, rank) {
if (rank === "parangon3" && result <= 3) return 4;
if (rank === "parangon2" && result <= 2) return 3;
if (rank === "parangon1" && result <= 1) return 2;
return result;
}
/**
* Update the aspect gauge on the actor after an aspect point roll.
* Gauge positive = solar side, negative = lunar side.
* ±5 → apply Déséquilibre. ±10 → full reset.
*/
async _updateAspectGauge() {
// Support both single-nested (system.aspects) and double-nested (system.aspects.aspects)
const aspectsPath = this._actor.system.aspects?.aspects !== undefined
? "system.aspects.aspects"
: "system.aspects";
const aspects = foundry.utils.getProperty(this._actor, aspectsPath) ?? {};
const gaugeDirection = this.object.aspectType === "solar" ? 1 : -1;
const newGauge = (aspects.gauge ?? 0) + gaugeDirection;
if (Math.abs(newGauge) >= 10) {
// Full reset
await this._actor.update({
[`${aspectsPath}.gauge`]: 0,
[`${aspectsPath}.solar`]: 0,
[`${aspectsPath}.lunar`]: 0,
});
// Remove all desequilibre conditions
const toRemove = this._actor.items
.filter((i) => i.type === "etat" && ["desequilibre_solaire", "desequilibre_lunaire"].includes(i.system?.condition_type))
.map((i) => i.id);
if (toRemove.length) {
await this._actor.deleteEmbeddedDocuments("Item", toRemove);
}
} else {
await this._actor.update({ [`${aspectsPath}.gauge`]: newGauge });
if (Math.abs(newGauge) >= 5) {
// Apply opposing desequilibre
const condType = this.object.aspectType === "solar" ? "desequilibre_lunaire" : "desequilibre_solaire";
const existing = this._actor.items.find(
(i) => i.type === "etat" && i.system?.condition_type === condType
);
if (!existing) {
await this._actor.createEmbeddedDocuments("Item", [
{
type: "etat",
name: game.i18n.localize(`chiaroscuro.aspects.${condType}`),
system: { condition_type: condType },
},
]);
}
}
}
}
/**
* Create and send the chat message.
*/
async _sendChatMessage(rollData) {
const content = await foundry.applications.handlebars.renderTemplate(
CONFIG.l5r5e.paths.templates + "dice/chiaroscuro-chat-roll.html",
{
actor: this._actor,
profileImg: this._actor?.img ?? "icons/svg/mystery-man.svg",
ring: this.object.ring,
skill: this.object.skill,
difficulty: this.object.difficulty,
useAspectPoint: this.object.useAspectPoint,
aspectType: this.object.aspectType,
useAssistance: this.object.useAssistance,
modifier: this.object.modifier,
quickInfo: this._actor?.system?.quick_info ?? "",
...rollData,
}
);
return ChatMessage.implementation.create({
user: game.user.id,
speaker: {
actor: this._actor?.id ?? null,
alias: this._actor?.name ?? null,
},
content,
sound: CONFIG.sounds.dice,
});
}
}

View File

@@ -513,6 +513,9 @@ export class DicePickerDialog extends FormApplication {
this._updateVoidPointUsage(); this._updateVoidPointUsage();
this.render(false); 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; 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});
}
}
} }

View File

@@ -268,6 +268,9 @@ export class RollnKeepDialog extends FormApplication {
], { jQuery: false }); ], { 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 *** // *** Everything below here is only needed if the sheet is editable ***
if (!this.isEditable) { if (!this.isEditable) {
return; return;
@@ -802,4 +805,34 @@ export class RollnKeepDialog extends FormApplication {
// Re-enable the button // Re-enable the button
button.attr("disabled", false); 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});
}
}
} }

View File

@@ -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 */ /** @override ApplicationV2 */

View File

@@ -5,10 +5,11 @@ export class GmToolbox extends HandlebarsApplicationMixin(ApplicationV2) {
/** @override ApplicationV2 */ /** @override ApplicationV2 */
static get DEFAULT_OPTIONS() { return { static get DEFAULT_OPTIONS() { return {
id: "l5r5e-gm-toolbox", id: "l5r5e-gm-toolbox",
classes: ["faded-ui"],
window: { window: {
contentClasses: ["l5r5e", "gm-toolbox", "faded-ui"], contentClasses: ["l5r5e", "gm-toolbox"],
title: "l5r5e.gm.toolbox.title", title: "l5r5e.gm.toolbox.title",
minimizable: true, minimizable: false,
}, },
position: { position: {
width: "auto", width: "auto",
@@ -89,6 +90,19 @@ export class GmToolbox extends HandlebarsApplicationMixin(ApplicationV2) {
options.position.left = 220; //x - 630; 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. * The GM Toolbox should not be removed when toggling the main menu with the esc key etc.
* @override ApplicationV2 * @override ApplicationV2

View File

@@ -64,6 +64,11 @@ export const RegisterHandlebars = function () {
return objects.join(""); return objects.join("");
}); });
// Chiaroscuro: return the flat bonus for a given skill rank id
Handlebars.registerHelper("skillRankBonus", (rankId) => {
return CONFIG.l5r5e.skillRanks?.[rankId]?.bonus ?? 0;
});
// Add a setter // Add a setter
Handlebars.registerHelper("setVar", function (varName, varValue, options) { Handlebars.registerHelper("setVar", function (varName, varValue, options) {
options.data.root[varName] = varValue; options.data.root[varName] = varValue;

View File

@@ -1,4 +1,4 @@
import { L5r5eHtmlMultiSelectElement } from "./misc/l5r5e-multiselect.js"; import { ItemCompendiumL5r5e } from "./compendium/l5r5e-item-compendium.js"
export default class HooksL5r5e { export default class HooksL5r5e {
/** /**
@@ -26,6 +26,8 @@ export default class HooksL5r5e {
) { ) {
game.babele.setSystemTranslationsDir("babele"); // Since Babele v2.0.7 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 * DiceSoNice - Add L5R DicePresets
*/ */

View File

@@ -149,5 +149,8 @@ export class AdvancementSheetL5r5e extends ItemSheetL5r5e {
xp_used: xp_used, xp_used: xp_used,
}, },
}); });
// Re-render sheet
this.render(true);
} }
} }

View File

@@ -0,0 +1,44 @@
import { BaseItemSheetL5r5e } from "./base-item-sheet.js";
/**
* Sheet for Arcane items (Chiaroscuro).
* @extends {BaseItemSheetL5r5e}
*/
export class ArcaneSheetL5r5e extends BaseItemSheetL5r5e {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["l5r5e", "sheet", "arcane"],
template: CONFIG.l5r5e.paths.templates + "items/arcane/arcane-sheet.html",
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "attributes" }],
});
}
/** @override */
async getData(options = {}) {
const sheetData = await super.getData(options);
// Convert application array to comma-separated string for display
const app = sheetData.data.system.application;
sheetData.data.system.applicationDisplay = Array.isArray(app) ? app.join(", ") : (app ?? "");
sheetData.data.enrichedHtml = {
description: await foundry.applications.ux.TextEditor.implementation.enrichHTML(
sheetData.data.system.description ?? "",
{ async: true }
),
};
return sheetData;
}
/** @override */
async _updateObject(event, formData) {
// Convert comma-separated application string back to array
const raw = formData["system.applicationDisplay"] ?? "";
formData["system.application"] = raw.split(",").map((s) => s.trim()).filter(Boolean);
delete formData["system.applicationDisplay"];
return super._updateObject(event, formData);
}
}

View File

@@ -11,4 +11,16 @@ export class ArmorSheetL5r5e extends ItemSheetL5r5e {
template: CONFIG.l5r5e.paths.templates + "items/armor/armor-sheet.html", template: CONFIG.l5r5e.paths.templates + "items/armor/armor-sheet.html",
}); });
} }
/** @override */
async getData(options = {}) {
const sheetData = await super.getData(options);
const catObj = game.l5r5e.HelpersL5r5e.getLocalizedRawObject("chiaroscuro.armor.categories") ?? {};
sheetData.data.armorCategories = Object.entries(catObj)
.filter(([k]) => k !== "label")
.map(([id, label]) => ({ id, label }));
return sheetData;
}
} }

View File

@@ -0,0 +1,30 @@
import { BaseItemSheetL5r5e } from "./base-item-sheet.js";
/**
* Sheet for État items (Chiaroscuro).
* @extends {BaseItemSheetL5r5e}
*/
export class EtatSheetL5r5e extends BaseItemSheetL5r5e {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["l5r5e", "sheet", "etat"],
template: CONFIG.l5r5e.paths.templates + "items/etat/etat-sheet.html",
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "attributes" }],
});
}
/** @override */
async getData(options = {}) {
const sheetData = await super.getData(options);
sheetData.data.enrichedHtml = {
description: await foundry.applications.ux.TextEditor.implementation.enrichHTML(
sheetData.data.system.description ?? "",
{ async: true }
),
};
return sheetData;
}
}

View File

@@ -0,0 +1,35 @@
import { BaseItemSheetL5r5e } from "./base-item-sheet.js";
/**
* Sheet for Mystère items (Chiaroscuro).
* @extends {BaseItemSheetL5r5e}
*/
export class MystereSheetL5r5e extends BaseItemSheetL5r5e {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["l5r5e", "sheet", "mystere"],
template: CONFIG.l5r5e.paths.templates + "items/mystere/mystere-sheet.html",
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "attributes" }],
});
}
/** @override */
async getData(options = {}) {
const sheetData = await super.getData(options);
sheetData.data.mystereTypes = [
{ id: "mineur", label: game.i18n.localize("chiaroscuro.mystere.mineur") },
{ id: "majeur", label: game.i18n.localize("chiaroscuro.mystere.majeur") },
];
sheetData.data.enrichedHtml = {
description: await foundry.applications.ux.TextEditor.implementation.enrichHTML(
sheetData.data.system.description ?? "",
{ async: true }
),
};
return sheetData;
}
}

View File

@@ -17,12 +17,27 @@ export class TechniqueSheetL5r5e extends ItemSheetL5r5e {
const sheetData = await super.getData(options); const sheetData = await super.getData(options);
// List all available techniques type // List all available techniques type
const types = ["core", "school", "title"]; const types = ["core", "school", "title", "chiaroscuro"];
if (game.settings.get(CONFIG.l5r5e.namespace, "techniques-customs")) { if (game.settings.get(CONFIG.l5r5e.namespace, "techniques-customs")) {
types.push("custom"); types.push("custom");
} }
sheetData.data.techniquesList = game.l5r5e.HelpersL5r5e.getTechniquesList({ types }); sheetData.data.techniquesList = game.l5r5e.HelpersL5r5e.getTechniquesList({ types });
// Invocation sub-type fields (visible only for mot_invocation)
sheetData.data.isMotInvocation = sheetData.data.system.technique_type === "mot_invocation";
sheetData.data.invocationTypes = [
{ id: "general", label: game.i18n.localize("chiaroscuro.technique.invocation_types.general") },
{ id: "neutre", label: game.i18n.localize("chiaroscuro.technique.invocation_types.neutre") },
{ id: "precis", label: game.i18n.localize("chiaroscuro.technique.invocation_types.precis") },
];
sheetData.data.modeInvocationValues = [
{ id: "-3", label: "-3" },
{ id: "0", label: "0" },
{ id: "3", label: "+3" },
];
// Convert mode_invocation to string for selectOptions matching
sheetData.data.system.mode_invocation_str = String(sheetData.data.system.mode_invocation ?? 0);
// Sanitize Difficulty and Skill list // Sanitize Difficulty and Skill list
sheetData.data.system.difficulty = TechniqueSheetL5r5e.formatDifficulty(sheetData.data.system.difficulty); sheetData.data.system.difficulty = TechniqueSheetL5r5e.formatDifficulty(sheetData.data.system.difficulty);
sheetData.data.system.skill = TechniqueSheetL5r5e.translateSkillsList( sheetData.data.system.skill = TechniqueSheetL5r5e.translateSkillsList(
@@ -55,6 +70,12 @@ export class TechniqueSheetL5r5e extends ItemSheetL5r5e {
TechniqueSheetL5r5e.translateSkillsList(formData["system.skill"].split(","), true) TechniqueSheetL5r5e.translateSkillsList(formData["system.skill"].split(","), true)
).join(","); ).join(",");
// Convert mode_invocation_str back to number
if ("system.mode_invocation_str" in formData) {
formData["system.mode_invocation"] = parseInt(formData["system.mode_invocation_str"] ?? "0", 10);
delete formData["system.mode_invocation_str"];
}
return super._updateObject(event, formData); return super._updateObject(event, formData);
} }

View File

@@ -23,6 +23,12 @@ export class WeaponSheetL5r5e extends ItemSheetL5r5e {
label: "l5r5e.skills." + cat.toLowerCase() + "." + id.toLowerCase(), label: "l5r5e.skills." + cat.toLowerCase() + "." + id.toLowerCase(),
})); }));
// Weapon categories (Chiaroscuro)
const catObj = game.l5r5e.HelpersL5r5e.getLocalizedRawObject("chiaroscuro.weapon.categories") ?? {};
sheetData.data.weaponCategories = [{ id: "", label: "—" }].concat(
Object.entries(catObj).map(([id, label]) => ({ id, label }))
);
return sheetData; return sheetData;
} }
} }

View File

@@ -11,6 +11,7 @@ import { ActorL5r5e } from "./actor.js";
import { CharacterSheetL5r5e } from "./actors/character-sheet.js"; import { CharacterSheetL5r5e } from "./actors/character-sheet.js";
import { NpcSheetL5r5e } from "./actors/npc-sheet.js"; import { NpcSheetL5r5e } from "./actors/npc-sheet.js";
import { ArmySheetL5r5e } from "./actors/army-sheet.js"; import { ArmySheetL5r5e } from "./actors/army-sheet.js";
import { RulerL5r5e, TokenRulerL5r5e } from "./tatical-grid-rulers.js";
// Dice and rolls // Dice and rolls
import { L5rBaseDie } from "./dice/dietype/l5r-base-die.js"; import { L5rBaseDie } from "./dice/dietype/l5r-base-die.js";
import { AbilityDie } from "./dice/dietype/ability-die.js"; import { AbilityDie } from "./dice/dietype/ability-die.js";
@@ -18,6 +19,7 @@ import { RingDie } from "./dice/dietype/ring-die.js";
import { RollL5r5e } from "./dice/roll.js"; import { RollL5r5e } from "./dice/roll.js";
import { DicePickerDialog } from "./dice/dice-picker-dialog.js"; import { DicePickerDialog } from "./dice/dice-picker-dialog.js";
import { RollnKeepDialog } from "./dice/roll-n-keep-dialog.js"; import { RollnKeepDialog } from "./dice/roll-n-keep-dialog.js";
import { ChiaroscuroDiceDialog } from "./dice/chiaroscuro-dice-dialog.js";
import { CombatL5r5e } from "./combat.js"; import { CombatL5r5e } from "./combat.js";
// Items // Items
import { ItemL5r5e } from "./item.js"; import { ItemL5r5e } from "./item.js";
@@ -32,11 +34,16 @@ import { TitleSheetL5r5e } from "./items/title-sheet.js";
import { BondSheetL5r5e } from "./items/bond-sheet.js"; import { BondSheetL5r5e } from "./items/bond-sheet.js";
import { SignatureScrollSheetL5r5e } from "./items/signature-scroll-sheet.js"; import { SignatureScrollSheetL5r5e } from "./items/signature-scroll-sheet.js";
import { ItemPatternSheetL5r5e } from "./items/item-pattern-sheet.js"; import { ItemPatternSheetL5r5e } from "./items/item-pattern-sheet.js";
import { ArcaneSheetL5r5e } from "./items/arcane-sheet.js";
import { EtatSheetL5r5e } from "./items/etat-sheet.js";
import { MystereSheetL5r5e } from "./items/mystere-sheet.js";
import { ArmyCohortSheetL5r5e } from "./items/army-cohort-sheet.js"; import { ArmyCohortSheetL5r5e } from "./items/army-cohort-sheet.js";
import { ArmyFortificationSheetL5r5e } from "./items/army-fortification-sheet.js"; import { ArmyFortificationSheetL5r5e } from "./items/army-fortification-sheet.js";
// JournalEntry // JournalEntry
import { JournalL5r5e } from "./journal.js"; import { JournalL5r5e } from "./journal.js";
import { BaseJournalSheetL5r5e } from "./journals/base-journal-sheet.js"; import { BaseJournalSheetL5r5e } from "./journals/base-journal-sheet.js";
// Compendium
import { CompendiumDirectoryL5r5e } from "./compendium/l5r5e-compendium-directory.js";
// Specific // Specific
import { MigrationL5r5e } from "./migration.js"; import { MigrationL5r5e } from "./migration.js";
import { GmToolbox } from "./gm/gm-toolbox.js"; import { GmToolbox } from "./gm/gm-toolbox.js";
@@ -44,8 +51,10 @@ import { GmMonitor } from "./gm/gm-monitor.js";
import { Storage } from "./storage.js"; import { Storage } from "./storage.js";
// Misc // Misc
import { L5r5eHtmlMultiSelectElement } from "./misc/l5r5e-multiselect.js"; import { L5r5eHtmlMultiSelectElement } from "./misc/l5r5e-multiselect.js";
import { L5R5eHtmlComboBoxElement } from "./misc/l5r5e-combo-box.js";
window.customElements.define(L5r5eHtmlMultiSelectElement.tagName, L5r5eHtmlMultiSelectElement); window.customElements.define(L5r5eHtmlMultiSelectElement.tagName, L5r5eHtmlMultiSelectElement);
window.customElements.define(L5R5eHtmlComboBoxElement.tagName, L5R5eHtmlComboBoxElement);
/* ------------------------------------ */ /* ------------------------------------ */
/* Initialize system */ /* Initialize system */
@@ -65,6 +74,23 @@ Hooks.once("init", async () => {
// Global access to L5R Config // Global access to L5R Config
CONFIG.l5r5e = L5R5E; 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 // Assign custom classes and constants here
CONFIG.Combat.documentClass = CombatL5r5e; CONFIG.Combat.documentClass = CombatL5r5e;
CONFIG.Actor.documentClass = ActorL5r5e; CONFIG.Actor.documentClass = ActorL5r5e;
@@ -72,6 +98,10 @@ Hooks.once("init", async () => {
CONFIG.Item.documentClass = ItemL5r5e; CONFIG.Item.documentClass = ItemL5r5e;
CONFIG.JournalEntry.documentClass = JournalL5r5e; CONFIG.JournalEntry.documentClass = JournalL5r5e;
CONFIG.JournalEntry.sheetClass = BaseJournalSheetL5r5e; CONFIG.JournalEntry.sheetClass = BaseJournalSheetL5r5e;
CONFIG.Token.rulerClass = TokenRulerL5r5e;
CONFIG.Canvas.rulerClass = RulerL5r5e;
CONFIG.ui.compendium = CompendiumDirectoryL5r5e;
// Define custom Roll class // Define custom Roll class
CONFIG.Dice.rolls.unshift(RollL5r5e); CONFIG.Dice.rolls.unshift(RollL5r5e);
@@ -92,6 +122,7 @@ Hooks.once("init", async () => {
ActorL5r5e, ActorL5r5e,
DicePickerDialog, DicePickerDialog,
RollnKeepDialog, RollnKeepDialog,
ChiaroscuroDiceDialog,
GmToolbox, GmToolbox,
GmMonitor, GmMonitor,
storage: new Storage(), storage: new Storage(),
@@ -197,6 +228,21 @@ Hooks.once("init", async () => {
label: "TYPES.Item.army_fortification", label: "TYPES.Item.army_fortification",
makeDefault: true, makeDefault: true,
}); });
fdc.Items.registerSheet(L5R5E.namespace, ArcaneSheetL5r5e, {
types: ["arcane"],
label: "TYPES.Item.arcane",
makeDefault: true,
});
fdc.Items.registerSheet(L5R5E.namespace, EtatSheetL5r5e, {
types: ["etat"],
label: "TYPES.Item.etat",
makeDefault: true,
});
fdc.Items.registerSheet(L5R5E.namespace, MystereSheetL5r5e, {
types: ["mystere"],
label: "TYPES.Item.mystere",
makeDefault: true,
});
// Journal // Journal
fdc.Journal.unregisterSheet("core", fav1s.JournalSheet); fdc.Journal.unregisterSheet("core", fav1s.JournalSheet);
@@ -262,6 +308,4 @@ Hooks.on("renderSidebarTab", (app, html, data) => HooksL5r5e.renderSidebarTab(ap
Hooks.on("activateSettings", async (app)=> HooksL5r5e.activateSettings(app)); Hooks.on("activateSettings", async (app)=> HooksL5r5e.activateSettings(app));
Hooks.on("renderChatMessageHTML", (message, html, data) => HooksL5r5e.renderChatMessage(message, html, data)); Hooks.on("renderChatMessageHTML", (message, html, data) => HooksL5r5e.renderChatMessage(message, html, data));
Hooks.on("renderCombatTracker", (app, html, data) => HooksL5r5e.renderCombatTracker(app, 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("diceSoNiceRollStart", (messageId, context) => HooksL5r5e.diceSoNiceRollStart(messageId, context));
Hooks.on("updateCompendium", (pack, documents, options, userId) => HooksL5r5e.updateCompendium(pack, documents, options, userId));

View File

@@ -309,6 +309,29 @@ export class MigrationL5r5e {
} }
// ***** End of 1.3.0 ***** // ***** End of 1.3.0 *****
// ***** Start of 2.0.0 (Chiaroscuro) *****
if (options?.force || MigrationL5r5e.needUpdate("2.0.0")) {
// Migrate character skill ranks from numeric (1-5) to Chiaroscuro string enum.
// Rank 0 (unranked) stays as the number 0 — this is intentional: the template
// defaults to 0 (number) and JS coerces it correctly when keying into skillRanks.
if (actor.type === "character" && system.skills) {
const rankMap = { 1: "initie", 2: "expert", 3: "maitre", 4: "parangon1", 5: "parangon2" };
const groups = ["artisan", "martial", "scholar", "social", "trade"];
for (const group of groups) {
const groupSkills = system.skills[group];
if (!groupSkills) continue;
for (const [skillName, rank] of Object.entries(groupSkills)) {
const numRank = Number(rank);
// Only migrate non-zero numeric ranks; 0 is already valid as-is
if (numRank > 0 && rankMap[numRank]) {
updateData[`system.skills.${group}.${skillName}`] = rankMap[numRank];
}
}
}
}
}
// ***** End of 2.0.0 (Chiaroscuro) *****
return updateData; return updateData;
} }

View 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 }));
}
}

View 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;
}
}
};
}

View File

@@ -1,301 +1,424 @@
import { DropdownMixin } from "./l5r5e-dropdown-mixin.js";
const { AbstractMultiSelectElement } = foundry.applications.elements; const { AbstractMultiSelectElement } = foundry.applications.elements;
/** /**
* Provide a multi-select workflow using a select element as the input mechanism. * A custom `<l5r5e-multi-select>` form element providing Select2-style chip multi-selection.
* 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
* *
* @example Multi-Select HTML Markup * Stores **multiple string values** from a fixed option list, shown as removable chips inside
* ```html * the input box. A live-search input filters the dropdown as the user types. Use this when a
* <l5r5e-multi-select name="select-many-things"> * field holds an unordered collection of values (e.g. a set of skills, tags, or abilities).
* <optgroup label="Basic Options"> * For storing a single value — predefined or free-text — use {@link L5R5eHtmlComboBoxElement} instead.
* <option value="foo">Foo</option> *
* <option value="bar">Bar</option> * The element's `value` getter returns a comma-separated string (e.g. `"fire,water"`),
* <option value="baz">Baz</option> * which is what `FormData` will read on submission. `_getValue()` returns a plain Array,
* </optgroup> * which is what `FormDataExtended` will use.
* <optgroup label="Advanced Options"> *
* <option value="fizz">Fizz</option> * Pre-selection on render is handled via the `value` attribute on the element — NOT via
* <option value="buzz">Buzz</option> * `{{selectOptions selected=...}}`, which cannot handle comma-separated strings. Use
* </optgroup> * `{{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> * </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 { export class L5r5eHtmlMultiSelectElement extends DropdownMixin(
AbstractMultiSelectElement,
constructor() { { multiSelect: true, debounceMs: 150 }
super(); ) {
this.#setup();
}
/** @override */ /** @override */
static tagName = "l5r5e-multi-select"; static tagName = "l5r5e-multi-select";
/** /** @type {HTMLDivElement} — outer box containing chips, input, clear button */
* A select element used to choose options. #selectionBox;
* @type {HTMLSelectElement}
*/ /** @type {HTMLDivElement} — chips are injected here */
#select; #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. * Returns a comma-separated string
* @type {HTMLDivElement} * 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. * Return an array so FormDataExtended.object[name] matches Foundry's own
* @type {HTMLButtonElement} * 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. * Accept either an array or comma-separated string when Foundry calls _setValue().
* @type {Set} * @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.");
// 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 this._value.clear();
// and we cannot override _initialize since we don't have access to #disabledValues there for (const v of values) {
#setup() { this._value.add(v);
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);
}
} }
} }
/** @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"));
}
}
/* -------------------------------------------- */
/* Element Lifecycle */
/* -------------------------------------------- */
/** @override */ /** @override */
_buildElements() { _buildElements() {
this.#setup(); // Ask mixin to build <input> + <ul>, then re-home them into our structure.
const mixinWrapper = this._buildDropdownElements({
// Create select element placeholder: this.getAttribute("placeholder")
this.#select = this._primaryInput = document.createElement("select"); ?? game.i18n.localize("l5r5e.multiselect.placeholder"),
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);
}); });
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. // Selection box
if($(this.#tags).css("max-height")) { this.#selectionBox = document.createElement("div");
const numericMaxHeight = parseInt($(this.#tags).css("max-height"), 10); this.#selectionBox.classList.add("selection-box");
if(numericMaxHeight) {
if($(this.#tags).prop("scrollHeight") > numericMaxHeight) {
this.#tags.classList.add("overflowing");
}
else {
this.#tags.classList.remove("overflowing");
}
}
}
// Disable selected options // Chip list
const hideDisabled = game.settings.get(CONFIG.l5r5e.namespace, "compendium-hide-disabled-sources"); this.#chipList = document.createElement("div");
for (const option of this.#select) { this.#chipList.classList.add("chip-list");
if (this._value.has(option.value)) {
option.disabled = true; // Auto-sizing sizer — CSS grid trick: ::after mirrors data-value, input shares the cell
option.title = game.i18n.localize("l5r5e.multiselect.already_in_filter"); this.#inputSizer = document.createElement("span");
continue; this.#inputSizer.classList.add("input-sizer");
} this.#inputSizer.dataset.value = "";
if (this.#disabledValues.has(option.value)) { this.#inputSizer.append(searchInput);
option.disabled = true;
option.hidden = hideDisabled; // Clear-all button
continue; this.#clearButton = document.createElement("button");
} this.#clearButton.type = "button";
option.disabled = false; this.#clearButton.classList.add("clear-btn");
option.removeAttribute("title"); 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 */ /** @override */
_activateListeners() { _activateListeners() {
this.#select.addEventListener("change", this.#onChangeSelect.bind(this)); this._activateDropdownListeners();
this.#clearAll.addEventListener("click", this.#onClickClearAll.bind(this)); const signal = this.abortSignal;
this.#tags.addEventListener("click", this.#onClickTag.bind(this));
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 */ /** @override */
_toggleDisabled(disabled) { _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) { 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 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 for (const groupEntry of groups) {
const select = document.createElement(L5r5eHtmlMultiSelectElement.tagName); let parent = element;
select.name = config.name; if (groupEntry.group) {
foundry.applications.fields.setInputAttributes(select, config); parent = _appendOptgroup(groupEntry.group, element);
for (const group_entry of groups) {
let parent = select;
if (group_entry.group) {
parent = _appendOptgroupHtml(group_entry.group, select);
} }
for (const option_entry of group_entry.options) { for (const groupOption of groupEntry.options){
_appendOptionHtml(option_entry, parent); _appendOption(groupOption, parent);
} }
} }
return select; return element;
} }
} }
/** Stolen from foundry.applications.fields.prepareSelectOptionGroups: Needed to add support for tooltips /* -------------------------------------------- */
* /* Module Helpers */
*/ /* -------------------------------------------- */
function prepareSelectOptionGroups(config) { function prepareSelectOptionGroups(config) {
const result = foundry.applications.fields.prepareSelectOptionGroups(config); const result = foundry.applications.fields.prepareSelectOptionGroups(config);
config.options.filter(option => option?.disabled || option?.tooltip).forEach(special => {
// Disable options based on input result.forEach(group => {
config.options.filter((option) => option?.disabled || option?.tooltip).forEach((SpecialOption) => { group.options.forEach(groupOption => {
result.forEach((group) => { if (groupOption.value === special.value) {
group.options.forEach((option) => { groupOption.disabled = special.disabled;
if (SpecialOption.value === option.value) { groupOption.tooltip = special.tooltip;
option.disabled = SpecialOption.disabled;
option.tooltip = SpecialOption?.tooltip;
} }
}) });
}) });
}) });
return result; return result;
} }
/** Stolen from foundry.applications.fields function _appendOptgroup(label, parent) {
* Create and append an optgroup element to a parent select. const element = document.createElement("optgroup");
* @param {string} label element.label = label;
* @param {HTMLSelectElement} parent parent.appendChild(element);
* @returns {HTMLOptGroupElement}
* @internal return element;
*/
function _appendOptgroupHtml(label, parent) {
const optgroup = document.createElement("optgroup");
optgroup.label = label;
parent.appendChild(optgroup);
return optgroup;
} }
/** Stolen from foundry.applications.fields function _appendOption(option, parent) {
* Create and append an option element to a parent select or optgroup.
* @param {FormSelectOption} option
* @param {HTMLSelectElement|HTMLOptGroupElement} parent
* @internal
*/
function _appendOptionHtml(option, parent) {
const { value, label, selected, disabled, rule, tooltip } = option; const { value, label, selected, disabled, rule, tooltip } = option;
if ((value !== undefined) && (label !== undefined)) { if (value !== undefined && label !== undefined) {
const option_html = document.createElement("option"); const element = document.createElement("option");
option_html.value = value; element.value = value;
option_html.innerText = label; element.innerText = label;
if (selected) { if (selected) {
option_html.toggleAttribute("selected", true); element.toggleAttribute("selected", true);
} }
if (disabled) { if (disabled) {
option_html.toggleAttribute("disabled", true); element.toggleAttribute("disabled", true);
} }
if (tooltip) { if (tooltip) {
option_html.setAttribute("title", tooltip); element.setAttribute("title", tooltip);
} }
parent.appendChild(option_html); parent.appendChild(element);
} }
if (rule) { if (rule) {
parent.insertAdjacentHTML("beforeend", "<hr>"); parent.insertAdjacentHTML("beforeend", "<hr>");

View File

@@ -5,12 +5,15 @@ export const PreloadTemplates = async function () {
// *** Actors : PC *** // *** Actors : PC ***
`${tpl}actors/character/advancement-school.html`, `${tpl}actors/character/advancement-school.html`,
`${tpl}actors/character/advancement-others.html`, `${tpl}actors/character/advancement-others.html`,
`${tpl}actors/character/aspects.html`,
`${tpl}actors/character/attributes.html`, `${tpl}actors/character/attributes.html`,
`${tpl}actors/character/category.html`, `${tpl}actors/character/category.html`,
`${tpl}actors/character/conflict.html`, `${tpl}actors/character/conflict.html`,
`${tpl}actors/character/experience.html`, `${tpl}actors/character/experience.html`,
`${tpl}actors/character/identity.html`, `${tpl}actors/character/identity.html`,
`${tpl}actors/character/identity-text.html`,
`${tpl}actors/character/inventory.html`, `${tpl}actors/character/inventory.html`,
`${tpl}actors/character/invocations.html`,
`${tpl}actors/character/narrative.html`, `${tpl}actors/character/narrative.html`,
`${tpl}actors/character/rings.html`, `${tpl}actors/character/rings.html`,
`${tpl}actors/character/effects.html`, `${tpl}actors/character/effects.html`,
@@ -74,5 +77,6 @@ export const PreloadTemplates = async function () {
`${tpl}items/weapon/weapon-sheet.html`, `${tpl}items/weapon/weapon-sheet.html`,
`${tpl}items/army-cohort/army-cohort-entry.html`, `${tpl}items/army-cohort/army-cohort-entry.html`,
`${tpl}items/army-fortification/army-fortification-entry.html`, `${tpl}items/army-fortification/army-fortification-entry.html`,
`${tpl}dice/chiaroscuro-chat-roll.html`,
]); ]);
}; };

View File

@@ -1,4 +1,5 @@
import { L5r5eSetField } from "./data/l5r5e-setfield.js"; import { L5r5eSetField } from "./data/l5r5e-setfield.js";
import { TacticalGridSettingsL5R5E } from "./settings/tactical-grid-settings.js"
/** /**
* Custom system settings register * Custom system settings register
@@ -236,4 +237,28 @@ export const RegisterSettings = function () {
default: [], default: [],
onChange: () => game.l5r5e.HelpersL5r5e.refreshLocalAndSocket("l5r5e-gm-monitor"), 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
});
}; };

View 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 modules
* 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 });
}
}

View 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;
}
}

View File

@@ -20,5 +20,7 @@
@import "../scss/skills"; @import "../scss/skills";
@import "../scss/items"; @import "../scss/items";
@import "../scss/twenty-questions"; @import "../scss/twenty-questions";
@import "../scss/tactical-grid";
@import "../scss/chiaroscuro";
} }
} }

File diff suppressed because one or more lines are too long

View File

@@ -10,8 +10,6 @@
.readiness { .readiness {
flex: 0 0 100%; flex: 0 0 100%;
display: flex;
flex-wrap: wrap;
ul { ul {
display: flex; display: flex;
@@ -21,15 +19,20 @@
li { li {
flex: 25%; flex: 25%;
display: inline-grid; display: flex;
flex-direction: column;
align-items: center;
position: relative; position: relative;
.attributes-buttons { .increment-control {
position: relative; position: absolute;
line-height: 13px; right: -0.70rem;
top: 0.3rem; top: 15%;
right: 1.2rem;
width: 12px; display: flex;
flex-direction: column;
gap:0.1rem;
line-height: 1;
} }
strong { strong {
@@ -40,14 +43,19 @@
} }
label { label {
flex: 100%; position: relative;
display: flex;
align-items: center;
justify-content: center;
} }
input { input {
background: transparent; background: transparent;
border: 0 none; border: 0 none;
text-align: center; text-align: center;
margin: 0.3rem 1.6rem 0 1.5rem; margin: 0;
padding: 0;
width: 2rem;
} }
&:after { &:after {
@@ -55,8 +63,6 @@
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
position: absolute; position: absolute;
right: calc(50% - 0.9rem);
top: 0.1rem;
background: transparent url("../assets/icons/circle.svg") no-repeat 0 0; background: transparent url("../assets/icons/circle.svg") no-repeat 0 0;
background-size: contain; background-size: contain;
opacity: 0.25; opacity: 0.25;
@@ -64,9 +70,6 @@
} }
&:nth-child(1) { &:nth-child(1) {
input {
margin: 0.3rem 1rem 0 1.5rem;
}
&:after { &:after {
transform: rotate(0deg); transform: rotate(0deg);
} }
@@ -79,9 +82,6 @@
} }
&:nth-child(3) { &:nth-child(3) {
input {
margin: 0.3rem 1rem 0 1.5rem;
}
&:after { &:after {
transform: rotate(180deg); transform: rotate(180deg);
} }

View File

@@ -0,0 +1,376 @@
// ── Chiaroscuro UI Styles ──────────────────────────────────────────────────
// ── Aspects (header block on character sheet) ─────────────────────────────
.aspects-section {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25rem 0.5rem;
padding: 0.25rem 0.5rem;
border: 1px solid rgba($chi-title, 0.3);
border-radius: 0.25rem;
font-size: 0.85rem;
.aspect-fields {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25rem;
flex: 1;
.attribute-label {
display: flex;
align-items: center;
gap: 0.25rem;
&:nth-child(1) { color: $chi-solar; } // solar
&:nth-child(2) { color: $chi-lunar; } // lunar
input[type="number"] {
width: 3rem;
text-align: center;
}
}
.gauge-bar-wrapper {
flex: 0 0 100%;
height: 0.4rem;
background: linear-gradient(to right, $chi-lunar, rgba(128,128,128,0.3) 50%, $chi-solar);
border-radius: 0.25rem;
position: relative;
overflow: hidden;
.gauge-bar {
position: absolute;
top: 0;
height: 100%;
border-radius: 0.25rem;
opacity: 0.8;
}
}
}
}
// ── État badges (character sheet header) ─────────────────────────────────
.etat-summary {
flex: 0 0 100%;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.2rem;
padding: 0.1rem 0;
font-size: 0.8rem;
.etat-summary-label {
color: $l5r5e-label;
font-style: italic;
margin-right: 0.2rem;
}
.etat-badge {
display: inline-flex;
align-items: center;
gap: 0.2rem;
padding: 0.1rem 0.35rem;
border-radius: 0.2rem;
background: rgba($chi-title, 0.18);
border: 1px solid rgba($chi-title, 0.4);
color: $chi-title;
cursor: default;
&:hover { background: rgba($chi-title, 0.32); }
}
}
// ── NPC Danger levels (identity.html) ────────────────────────────────────
.danger-row {
display: flex;
align-items: center;
gap: 0.4rem;
margin: 0.2rem 0;
}
.danger-wrapper {
display: flex;
align-items: center;
gap: 0.3rem;
.danger-select {
font-size: 0.85rem;
background: $l5r5e-white;
border: 0 none;
color: $l5r5e-bold;
font-family: $font-primary;
}
}
.danger-icons {
display: flex;
gap: 0.15rem;
.danger-icon {
font-size: 0.9rem;
&.fa-skull { color: $l5r5e-red; }
&.fa-star { color: $l5r5e-shuji; }
}
}
// ── Chiaroscuro Dice Dialog ───────────────────────────────────────────────
&.chiaroscuro-dice-dialog {
// Header: portrait + actor name
.chi-dice-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border-bottom: 1px solid rgba($chi-title, 0.3);
.profile-img {
width: 3rem;
height: 3rem;
object-fit: cover;
border-radius: 50%;
border: 2px solid rgba($chi-title, 0.5);
}
.chi-dice-actor-info {
display: flex;
flex-direction: column;
strong { font-family: $font-secondary; font-size: 1.1rem; color: $chi-title; }
.chi-dice-quick-info { font-size: 0.8rem; color: $l5r5e-label; font-style: italic; }
}
}
// Section fieldsets
.chi-dice-section {
flex: 0 0 100%;
border: 1px solid rgba($chi-subtitle, 0.35);
border-radius: 0.25rem;
margin: 0.35rem 0.5rem 0;
padding: 0.25rem 0.5rem 0.4rem;
legend {
font-family: $font-tertiary;
font-size: 0.8rem;
color: $chi-subtitle;
padding: 0 0.25rem;
}
}
// Ring selector
.chi-rings {
display: flex;
flex-wrap: wrap;
gap: 0.2rem;
list-style: none;
padding: 0;
margin: 0;
li { flex: 1; }
.ring-selection-chi {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.2rem;
border-radius: 0.2rem;
border: 1px solid rgba($l5r5e-title, 0.2);
cursor: pointer;
font-size: 0.75rem;
&:hover { border-color: rgba($chi-title, 0.6); background: rgba($chi-title, 0.08); }
&.ring-selected {
border-color: $chi-title;
background: rgba($chi-title, 0.15);
strong { text-decoration: underline; }
}
i { font-size: 1.5rem; }
.ring-value { font-weight: bold; font-size: 0.9rem; }
}
.earth.ring-selection-chi { color: $l5r5e-earth; }
.air.ring-selection-chi { color: $l5r5e-air; }
.water.ring-selection-chi { color: $l5r5e-water; }
.fire.ring-selection-chi { color: $l5r5e-fire; }
.void.ring-selection-chi { color: $l5r5e-void-light; }
}
// Skill info row
.chi-skill-row {
display: flex;
align-items: baseline;
gap: 0.4rem;
.chi-skill-name { font-weight: bold; flex: 1; }
.chi-skill-rank { font-size: 0.8rem; color: $l5r5e-label; }
.chi-skill-bonus { font-size: 0.85rem; color: $chi-solar; font-weight: bold; }
}
// Difficulty + modifier row
.chi-difficulty-row {
display: flex;
align-items: center;
gap: 0.5rem;
select { flex: 1; }
.chi-modifier-label {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.85rem;
white-space: nowrap;
}
}
// Options checkboxes
.chi-options-row {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.15rem 0;
label {
display: flex;
align-items: center;
gap: 0.35rem;
font-size: 0.9rem;
}
.chi-auto-aspect { font-size: 0.8rem; color: $l5r5e-label; }
}
// Dice total summary
.chi-dice-total-summary {
flex: 0 0 100%;
text-align: center;
padding: 0.4rem;
font-size: 0.9rem;
.chi-total-dice { font-size: 1.3rem; color: $chi-title; margin: 0 0.2rem; }
}
// Submit button
.chi-dice-submit {
flex: 0 0 100%;
padding: 0.4rem 0.5rem;
button[type="submit"] {
width: 100%;
background: rgba($chi-title, 0.85);
border: 1px solid $chi-title;
color: $white;
font-family: $font-tertiary;
font-size: 1rem;
padding: 0.4rem;
cursor: pointer;
border-radius: 0.2rem;
&:hover { background: $chi-title; }
}
}
}
// ── Chiaroscuro Chat Roll ─────────────────────────────────────────────────
.chiaroscuro-chat-roll {
padding: 0.35rem;
font-size: 0.9rem;
// Header: portrait + actor + badges
.chi-chat-header {
display: flex;
align-items: center;
gap: 0.4rem;
margin-bottom: 0.3rem;
.profile-img {
width: 2.5rem;
height: 2.5rem;
object-fit: cover;
border-radius: 50%;
border: 2px solid rgba($chi-title, 0.5);
}
.chi-chat-actor {
flex: 1;
strong { font-family: $font-secondary; color: $chi-title; }
.chi-chat-quick-info { font-size: 0.75rem; color: $l5r5e-label; font-style: italic; }
}
.chi-chat-badges {
display: flex;
gap: 0.2rem;
align-items: center;
.chi-aspect-badge {
font-size: 1rem;
&.solar { color: $chi-solar; }
&.lunar { color: $chi-lunar; }
}
.chi-assistance-badge { color: $l5r5e-label; font-size: 1rem; }
}
}
// Description line
.chi-chat-desc {
display: flex;
align-items: center;
gap: 0.3rem;
margin-bottom: 0.3rem;
font-size: 0.85rem;
.chi-chat-skill { font-weight: bold; color: $chi-title; }
.chi-chat-vs { color: $l5r5e-label; }
.chi-chat-diff { color: $chi-subtitle; font-style: italic; }
}
// Dice pool
.chi-chat-dice-pool {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-bottom: 0.35rem;
.chi-die {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.8rem;
height: 1.8rem;
border-radius: 0.25rem;
font-weight: bold;
font-size: 1rem;
background: rgba($l5r5e-black, 0.35);
border: 1px solid rgba($l5r5e-title, 0.4);
position: relative;
&.die-low { color: $l5r5e-red; border-color: rgba($l5r5e-red, 0.5); }
&.die-high { color: $chi-solar; border-color: rgba($chi-solar, 0.5); }
.die-adj-icon {
position: absolute;
top: -0.3rem;
right: -0.2rem;
font-size: 0.6rem;
color: $chi-solar;
}
}
}
// Breakdown
.chi-chat-breakdown {
font-size: 0.85rem;
margin-bottom: 0.25rem;
color: $l5r5e-label;
strong { color: $white; }
}
// Result banner
.chi-chat-result {
display: flex;
align-items: center;
gap: 0.3rem;
font-family: $font-tertiary;
font-size: 1rem;
padding: 0.25rem 0.5rem;
border-radius: 0.2rem;
&.chi-success {
background: rgba($chi-subtitle, 0.25);
border: 1px solid rgba($chi-subtitle, 0.5);
color: $chi-subtitle;
}
&.chi-failure {
background: rgba($l5r5e-red, 0.12);
border: 1px solid rgba($l5r5e-red, 0.35);
color: $l5r5e-red;
}
.chi-bonus-successes {
font-size: 0.85rem;
opacity: 0.85;
}
}
}
// ── Skill rank: bonus display ──────────────────────────────────────────────
.skill-bonus {
color: $chi-solar;
font-size: 0.8rem;
font-weight: bold;
margin-left: 0.2rem;
}
// ── Default ring indicator ────────────────────────────────────────────────
.rings .default-ring {
text-decoration: underline 2px $chi-title;
text-underline-offset: 2px;
cursor: pointer;
}

View File

@@ -53,7 +53,23 @@ $l5r5e-chat-color-roll: rgba(225, 215, 200, 0.75);
$l5r5e-chat-color-blind: transparent; $l5r5e-chat-color-blind: transparent;
$l5r5e-chat-color-whisper: rgba(225, 200, 225, 0.75); $l5r5e-chat-color-whisper: rgba(225, 200, 225, 0.75);
// -- Rings // Misc
$l5r5e-selection-circle-color: #8a1a00;
// -- Chiaroscuro Colors
// Title (was: rgba(186, 187, 177, 0.5) → spec: rgb(158, 65, 76))
$chi-title: rgb(158, 65, 76);
// Subtitle (spec: rgb(103, 128, 119))
$chi-subtitle: rgb(103, 128, 119);
// Solar aspect (spec: rgb(150, 119, 116))
$chi-solar: rgb(150, 119, 116);
// Lunar aspect (spec: rgb(100, 147, 137))
$chi-lunar: rgb(100, 147, 137);
// Active tab accent
$chi-tab-active: rgb(158, 65, 76);
// Tab hover accent
$chi-tab-hover: rgb(103, 128, 119);
// Earth // Earth
$l5r5e-earth: rgb(105, 150, 120); $l5r5e-earth: rgb(105, 150, 120);
@@ -65,6 +81,7 @@ $l5r5e-water: rgb(95, 145, 155);
$l5r5e-fire: rgb(155, 115, 80); $l5r5e-fire: rgb(155, 115, 80);
// Void // Void
$l5r5e-void: rgb(75, 70, 65); $l5r5e-void: rgb(75, 70, 65);
$l5r5e-void-light: rgba(207,207,207,.8);
// -- Clans // -- Clans

View File

@@ -5,9 +5,55 @@
cursor: url("../assets/cursors/pointer.webp"), pointer; 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
.dice-picker-dialog { .dice-picker-dialog {
min-width: 35rem; .effects {
@include roll-effects-base();
}
width: 35rem;
min-height: auto; min-height: auto;
// Utility // Utility
* { * {
@@ -206,6 +252,12 @@
.profil { .profil {
border-bottom: 1px solid rgba(0, 0, 0, 0.1); 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 { .dropbox {

View File

@@ -1,5 +1,5 @@
.application { .application {
color: var(--color-text-dark-primary); color: var(--color-text-primary);
.scrollable { .scrollable {
--scroll-margin: 0; --scroll-margin: 0;
@@ -48,10 +48,69 @@
} }
} }
// 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"
);
@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: 95%;
height: 95%;
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 { #sidebar-tabs > menu {
gap: 4px; // halve the distance between menu icons gap: 4px; // halve the distance between menu icons
} }
#sidebar-content.expanded { #sidebar-content.expanded {
background: url("../assets/ui/bgSidebar.webp") no-repeat; background: url("../assets/ui/bgSidebar.webp") no-repeat;
border-image: url("../assets/ui/macro-button.webp") 10 repeat; border-image: url("../assets/ui/macro-button.webp") 10 repeat;

View File

@@ -28,7 +28,7 @@ nav {
.item { .item {
flex: 1; flex: 1;
&:hover { &:hover {
background-color: $l5r5e-label; background-color: $chi-tab-hover;
color: $white-light; color: $white-light;
text-shadow: none; text-shadow: none;
clip-path: polygon( clip-path: polygon(
@@ -44,7 +44,7 @@ nav {
} }
} }
.item.active { .item.active {
background-color: rgba(73, 12, 11, 0.85); background-color: $chi-tab-active;
color: rgba(255, 255, 255, 1); color: rgba(255, 255, 255, 1);
clip-path: polygon( clip-path: polygon(
0% var(--notchSize), 0% var(--notchSize),
@@ -58,7 +58,7 @@ nav {
); );
&:hover { &:hover {
background-color: rgba(73, 12, 11, 0.85); background-color: $chi-tab-active;
cursor: default; cursor: default;
} }
} }

View File

@@ -326,7 +326,7 @@
margin: 0.25rem 0; margin: 0.25rem 0;
flex-direction: row-reverse; flex-direction: row-reverse;
strong { strong {
flex: 0 0 calc(100% - 3.5rem); flex: 0 0 calc(100% - 4.5rem);
} }
input { input {
flex: 0 0 3rem; flex: 0 0 3rem;
@@ -444,7 +444,7 @@
width: 3.5rem; width: 3.5rem;
} }
} }
.attributes-buttons { .increment-control {
line-height: 13px; line-height: 13px;
position: relative; position: relative;
top: 0.2rem; top: 0.2rem;
@@ -751,12 +751,22 @@
flex: calc(100% / 3); flex: calc(100% / 3);
padding: 0.5rem; padding: 0.5rem;
font-size: 0.85rem; font-size: 0.85rem;
align-items: center;
input { input {
margin-left: 0.5rem; margin-left: 0.5rem;
} }
} }
.xp-buttons,
.money-buttons { .money-buttons {
line-height: 13px; line-height: 13px;
padding-left: 0.3em;
}
.increment-control {
display: flex;
flex-direction: column;
align-items: center;
padding-left: 0.2rem;
flex: none;
} }
} }

View 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

View File

@@ -1,5 +1,5 @@
{ {
"id": "l5r5e", "id": "l5rx-chiaroscuro",
"title": "Legend of the Five Rings (5th Edition)", "title": "Legend of the Five Rings (5th Edition)",
"description": "This is an authorised multilingual game system En|Fr|Es, for Legend of the Five Rings (5th Edition) by <a href='https://edge-studio.net/'>Edge Studio</a> <p> - Join the official Discord server: <a href='https://discord.gg/foundryvtt'> Official Discord</a></p><p> - Rejoignez la communauté Francophone: <a href='https://discord.gg/pPSDNJk'>Francophone Discord</a></p>", "description": "This is an authorised multilingual game system En|Fr|Es, for Legend of the Five Rings (5th Edition) by <a href='https://edge-studio.net/'>Edge Studio</a> <p> - Join the official Discord server: <a href='https://discord.gg/foundryvtt'> Official Discord</a></p><p> - Rejoignez la communauté Francophone: <a href='https://discord.gg/pPSDNJk'>Francophone Discord</a></p>",
"url": "https://gitlab.com/teaml5r/l5r5e", "url": "https://gitlab.com/teaml5r/l5r5e",
@@ -7,8 +7,8 @@
"changelog": "https://gitlab.com/teaml5r/l5r5e/-/blob/master/CHANGELOG.md", "changelog": "https://gitlab.com/teaml5r/l5r5e/-/blob/master/CHANGELOG.md",
"license": "https://gitlab.com/teaml5r/l5r5e/-/blob/master/LICENSE.md", "license": "https://gitlab.com/teaml5r/l5r5e/-/blob/master/LICENSE.md",
"manifest": "https://gitlab.com/teaml5r/l5r5e/-/raw/master/system/system.json", "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", "download": "https://gitlab.com/teaml5r/l5r5e/-/jobs/artifacts/v1.13.4/raw/l5r5e.zip?job=build",
"version": "1.13.2", "version": "1.13.4",
"compatibility": { "compatibility": {
"minimum": "13", "minimum": "13",
"verified": "13", "verified": "13",
@@ -29,6 +29,10 @@
"name": "Carter", "name": "Carter",
"discord": "Carter#2703", "discord": "Carter#2703",
"url": "https://fr.tipeee.com/carter-foundryvtt" "url": "https://fr.tipeee.com/carter-foundryvtt"
},
{
"name": "Litasa",
"discord": "Litasa#3139"
} }
], ],
"background": "systems/l5r5e/assets/l5r-header.webp", "background": "systems/l5r5e/assets/l5r-header.webp",

View File

@@ -10,6 +10,8 @@
"age": "", "age": "",
"clan": "", "clan": "",
"family": "", "family": "",
"region": "",
"education": "",
"female": null, "female": null,
"marital_status": "", "marital_status": "",
"roles": "", "roles": "",
@@ -36,6 +38,7 @@
"status": 0, "status": 0,
"ninjo": "", "ninjo": "",
"giri": "", "giri": "",
"past_problems": "",
"bushido_tenets": { "bushido_tenets": {
"paramount": "", "paramount": "",
"less_significant": "" "less_significant": ""
@@ -118,18 +121,34 @@
"mantra": false, "mantra": false,
"specificity": true "specificity": true
} }
},
"aspects": {
"aspects": {
"solar": 0,
"lunar": 0,
"gauge": 0
}
} }
}, },
"character": { "character": {
"templates": ["softlock", "identity", "rings", "social", "skills", "techniques", "conflict", "advancement"], "templates": ["softlock", "identity", "rings", "social", "skills", "techniques", "conflict", "advancement", "aspects"],
"template": "core", "template": "core",
"twenty_questions": {}, "is_samurai": true,
"zeni": 0 "quick_info": "",
"default_ring": "void",
"koku": 0,
"bu": 0,
"zeni": 0,
"identity_text1": "",
"identity_text2": "",
"twenty_questions": {}
}, },
"npc": { "npc": {
"templates": ["softlock", "identity", "rings", "social", "techniques", "conflict"], "templates": ["softlock", "identity", "rings", "social", "techniques", "conflict"],
"type": "adversary", "type": "adversary",
"attitude": "", "attitude": "",
"martial_danger": "simple",
"social_danger": "simple",
"conflict_rank": { "conflict_rank": {
"martial": 0, "martial": 0,
"social": 0 "social": 0
@@ -194,7 +213,10 @@
"signature_scroll", "signature_scroll",
"item_pattern", "item_pattern",
"army_cohort", "army_cohort",
"army_fortification" "army_fortification",
"arcane",
"etat",
"mystere"
], ],
"templates": { "templates": {
"basics": { "basics": {
@@ -224,10 +246,12 @@
} }
}, },
"item": { "item": {
"templates": ["basics", "item"] "templates": ["basics", "item"],
"item_type": ""
}, },
"armor": { "armor": {
"templates": ["basics", "item"], "templates": ["basics", "item"],
"armor_category": "",
"armor": { "armor": {
"physical": 0, "physical": 0,
"supernatural": 0 "supernatural": 0
@@ -236,6 +260,7 @@
"weapon": { "weapon": {
"templates": ["basics", "item"], "templates": ["basics", "item"],
"category": "", "category": "",
"bonus": 0,
"skill": "melee", "skill": "melee",
"readied": false, "readied": false,
"range": "0", "range": "0",
@@ -248,7 +273,9 @@
"templates": ["basics", "advancement"], "templates": ["basics", "advancement"],
"skill": "", "skill": "",
"difficulty": "", "difficulty": "",
"technique_type": "kata" "technique_type": "kata",
"invocation_type": "",
"mode_invocation": 0
}, },
"property": { "property": {
"templates": ["basics"], "templates": ["basics"],
@@ -303,6 +330,27 @@
"difficulty": 0, "difficulty": 0,
"attrition_reduction": 0, "attrition_reduction": 0,
"notes": "" "notes": ""
},
"arcane": {
"templates": ["basics"],
"arcane_type": "",
"application": [],
"bonus": 2,
"progression": "",
"xp_cost": 1
},
"etat": {
"templates": ["basics"],
"application": "",
"mod": 0,
"effect": "",
"elimination": ""
},
"mystere": {
"templates": ["basics"],
"mystere_type": "mineur",
"prerequisite_skill": "",
"prerequisite_condition": ""
} }
} }
} }

View File

@@ -13,9 +13,9 @@
<div class="readiness"> <div class="readiness">
<ul> <ul>
<li> <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}}/> <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-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> <i class="addsub-control pointer-choice fa fa-minus-square" data-type="casualties" data-value="-1"></i>
</span> </span>
@@ -23,15 +23,15 @@
<strong>{{localize 'l5r5e.army.battle_readiness.casualties'}}</strong> <strong>{{localize 'l5r5e.army.battle_readiness.casualties'}}</strong>
</li> </li>
<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}}/> <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> </label>
<strong>{{localize 'l5r5e.army.battle_readiness.strength'}}</strong> <strong>{{localize 'l5r5e.army.battle_readiness.strength'}}</strong>
</li> </li>
<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}}/> <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-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> <i class="addsub-control pointer-choice fa fa-minus-square" data-type="panic" data-value="-1"></i>
</span> </span>
@@ -39,7 +39,7 @@
<strong>{{localize 'l5r5e.army.battle_readiness.panic'}}</strong> <strong>{{localize 'l5r5e.army.battle_readiness.panic'}}</strong>
</li> </li>
<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}}/> <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> </label>
<strong>{{localize 'l5r5e.army.battle_readiness.discipline'}}</strong> <strong>{{localize 'l5r5e.army.battle_readiness.discipline'}}</strong>

View File

@@ -28,6 +28,9 @@
{{> 'systems/l5r5e/templates/actors/character/attributes.html'}} {{> 'systems/l5r5e/templates/actors/character/attributes.html'}}
</div> </div>
</div> </div>
<div class="header-fields chiaroscuro-aspects-wrapper">
{{> 'systems/l5r5e/templates/actors/character/aspects.html'}}
</div>
</header> </header>
{{!-- Sheet Body --}} {{!-- Sheet Body --}}
<section class="sheet-body"> <section class="sheet-body">
@@ -38,9 +41,11 @@
<nav class="sheet-tabs tabs" data-group="primary"> <nav class="sheet-tabs tabs" data-group="primary">
<a class="item" data-tab="skills">{{localize 'l5r5e.skills.title'}}</a> <a class="item" data-tab="skills">{{localize 'l5r5e.skills.title'}}</a>
<a class="item" data-tab="narrative">{{localize 'l5r5e.sheets.narrative'}}</a> <a class="item" data-tab="narrative">{{localize 'l5r5e.sheets.narrative'}}</a>
<a class="item" data-tab="invocations">{{localize 'chiaroscuro.tabs.invocations'}}</a>
<a class="item" data-tab="conflict">{{localize 'l5r5e.conflict.title'}}</a> <a class="item" data-tab="conflict">{{localize 'l5r5e.conflict.title'}}</a>
<a class="item" data-tab="inventory">{{localize 'l5r5e.sheets.inventory'}}</a> <a class="item" data-tab="inventory">{{localize 'l5r5e.sheets.inventory'}}</a>
<a class="item" data-tab="experience">{{localize 'l5r5e.sheets.experience'}}</a> <a class="item" data-tab="experience">{{localize 'l5r5e.sheets.experience'}}</a>
<a class="item" data-tab="identity">{{localize 'chiaroscuro.tabs.identity'}}</a>
</nav> </nav>
{{!-- Skills Tab --}} {{!-- Skills Tab --}}
@@ -72,5 +77,15 @@
<article class="tab experience" data-group="primary" data-tab="experience"> <article class="tab experience" data-group="primary" data-tab="experience">
{{> 'systems/l5r5e/templates/actors/character/experience.html'}} {{> 'systems/l5r5e/templates/actors/character/experience.html'}}
</article> </article>
{{!-- Invocations Tab --}}
<article class="tab invocations" data-group="primary" data-tab="invocations">
{{> 'systems/l5r5e/templates/actors/character/invocations.html'}}
</article>
{{!-- Identity Tab --}}
<article class="tab identity" data-group="primary" data-tab="identity">
{{> 'systems/l5r5e/templates/actors/character/identity-text.html'}}
</article>
</section> </section>
</form> </form>

View File

@@ -0,0 +1,30 @@
<div class="aspects-section">
<div class="aspect-fields">
<label class="attribute-label">
{{localize 'chiaroscuro.aspects.solar'}}
<input type="number" name="system.aspects.aspects.solar" value="{{data.system.aspects.aspects.solar}}" class="select-on-focus" data-dtype="Number" min="0" max="100" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
</label>
<label class="attribute-label">
{{localize 'chiaroscuro.aspects.lunar'}}
<input type="number" name="system.aspects.aspects.lunar" value="{{data.system.aspects.aspects.lunar}}" class="select-on-focus" data-dtype="Number" min="0" max="100" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
</label>
<label class="attribute-label">
{{localize 'chiaroscuro.aspects.gauge'}}
<input type="number" name="system.aspects.aspects.gauge" value="{{data.system.aspects.aspects.gauge}}" class="select-on-focus" data-dtype="Number" min="-10" max="10" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
</label>
<div class="gauge-bar-wrapper">
<div class="gauge-bar" style="width: {{data.aspectsData.gaugePercent}}%; background-color: {{data.aspectsData.gaugeColor}};"></div>
</div>
</div>
{{#if data.etatItems.length}}
<div class="etat-summary">
<span class="etat-summary-label">{{localize 'chiaroscuro.etat.title'}}</span>
{{#each data.etatItems as |etat|}}
<span class="etat-badge l5r5e-tooltip" title="{{etat.system.effect}}">
<img src="{{etat.img}}" width="16" height="16" />
{{etat.name}}
</span>
{{/each}}
</div>
{{/if}}
</div>

View File

@@ -7,7 +7,7 @@
<label class="attribute-label"> <label class="attribute-label">
<strong>{{localize 'l5r5e.attributes.fatigue'}}</strong> <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}}/> <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-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> <i class="addsub-control pointer-choice fa fa-minus-square" data-type="fatigue" data-value="-1"></i>
</span> </span>
@@ -22,7 +22,7 @@
<label class="attribute-label"> <label class="attribute-label">
<strong>{{localize 'l5r5e.attributes.strife'}}</strong> <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}}/> <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-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> <i class="addsub-control pointer-choice fa fa-minus-square" data-type="strife" data-value="-1"></i>
</span> </span>

View File

@@ -3,6 +3,10 @@
<label class="attribute-label"> <label class="attribute-label">
{{localize 'l5r5e.advancements.total'}} {{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}}/> <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>
<label class="attribute-label"> <label class="attribute-label">
{{localize 'l5r5e.advancements.spent'}} {{localize 'l5r5e.advancements.spent'}}

View File

@@ -0,0 +1,10 @@
<div class="identity-text-wrapper flexrow">
<fieldset class="identity-text-block">
<legend class="text-block-header">{{localize 'chiaroscuro.tabs.identity_text1'}}</legend>
{{editor data.enrichedHtml.identity_text1 target="system.identity_text1" button=true editable=options.editable engine="prosemirror" collaborate=false}}
</fieldset>
<fieldset class="identity-text-block">
<legend class="text-block-header">{{localize 'chiaroscuro.tabs.identity_text2'}}</legend>
{{editor data.enrichedHtml.identity_text2 target="system.identity_text2" button=true editable=options.editable engine="prosemirror" collaborate=false}}
</fieldset>
</div>

View File

@@ -1,21 +1,27 @@
<ul class="identity-content"> <ul class="identity-content">
<li>
<label class="attribute-label is-samurai-label">
{{localize 'chiaroscuro.character.is_samurai'}}
<input type="checkbox" name="system.is_samurai" {{checked data.system.is_samurai}} {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
</label>
</li>
<li> <li>
<label class="attribute-label"> <label class="attribute-label">
{{#ifCond data.system.template '==' 'pow'}} {{#if data.system.is_samurai}}
{{localize 'l5r5e.sheets.region'}}
{{else}}
{{localize 'l5r5e.clans.label'}} {{localize 'l5r5e.clans.label'}}
{{/ifCond}} {{else}}
{{localize 'chiaroscuro.character.region'}}
{{/if}}
<input type="text" name="system.identity.clan" value="{{data.system.identity.clan}}" {{^if data.editable_not_soft_locked}}disabled{{/if}}/> <input type="text" name="system.identity.clan" value="{{data.system.identity.clan}}" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
</label> </label>
</li> </li>
<li> <li>
<label class="attribute-label"> <label class="attribute-label">
{{#ifCond data.system.template '==' 'pow'}} {{#if data.system.is_samurai}}
{{localize 'l5r5e.sheets.upbringing'}}
{{else}}
{{localize 'l5r5e.sheets.family'}} {{localize 'l5r5e.sheets.family'}}
{{/ifCond}} {{else}}
{{localize 'chiaroscuro.character.education'}}
{{/if}}
<input type="text" name="system.identity.family" value="{{data.system.identity.family}}" {{^if data.editable_not_soft_locked}}disabled{{/if}}/> <input type="text" name="system.identity.family" value="{{data.system.identity.family}}" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
</label> </label>
</li> </li>
@@ -37,4 +43,10 @@
<input type="text" name="system.identity.roles" value="{{data.system.identity.roles}}" {{^if data.editable_not_soft_locked}}disabled{{/if}}/> <input type="text" name="system.identity.roles" value="{{data.system.identity.roles}}" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
</label> </label>
</li> </li>
<li>
<label class="attribute-label quick-info-label">
{{localize 'chiaroscuro.character.quick_info'}}
<input type="text" name="system.quick_info" value="{{data.system.quick_info}}" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
</label>
</li>
</ul> </ul>

View File

@@ -1,25 +1,25 @@
<fieldset class="money money-wrapper"> <fieldset class="money money-wrapper">
<legend class="section-header">{{localize 'l5r5e.money.title'}}</legend> <legend class="section-header">{{localize 'l5r5e.money.title'}}</legend>
<label> <label class="attribute-label money">
{{localize 'l5r5e.money.koku'}} {{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}}/> <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-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> <i class="money-control pointer-choice fa fa-minus-square" data-type="koku" data-value="-1"></i>
</span> </span>
</label> </label>
<label> <label class="attribute-label money">
{{localize 'l5r5e.money.bu'}} {{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}}/> <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-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> <i class="money-control pointer-choice fa fa-minus-square" data-type="bu" data-value="-1"></i>
</span> </span>
</label> </label>
<label> <label class="attribute-label money">
{{localize 'l5r5e.money.zeni'}} {{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}}/> <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-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> <i class="money-control pointer-choice fa fa-minus-square" data-type="zeni" data-value="-1"></i>
</span> </span>

View File

@@ -0,0 +1,17 @@
<div class="invocations-wrapper">
{{#each data.splitInvocationsList as |list type|}}
<fieldset class="section-header flexrow">
<legend class="technique-controls">
<span>{{localize (concat 'chiaroscuro.technique.invocation_types.' type)}}</span>
{{#if ../data.editable_not_soft_locked}}
<a data-item-type="technique" class="technique-control item-add" data-tech-type="mot_invocation" title="{{localize 'l5r5e.global.add'}}"><i class="fas fa-plus"></i></a>
{{/if}}
</legend>
<ul class="item-list">
{{#each list as |item|}}
{{> 'systems/l5r5e/templates/items/technique/technique-entry.html' technique=item editable=../../data.editable_not_soft_locked}}
{{/each}}
</ul>
</fieldset>
{{/each}}
</div>

View File

@@ -7,12 +7,16 @@
<textarea type="text" name="system.social.ninjo" {{^if data.editable_not_soft_locked}}disabled{{/if}}>{{data.system.social.ninjo}}</textarea> <textarea type="text" name="system.social.ninjo" {{^if data.editable_not_soft_locked}}disabled{{/if}}>{{data.system.social.ninjo}}</textarea>
</label> </label>
<label class="attribute-label"> <label class="attribute-label">
{{#ifCond data.system.template '==' 'pow'}} {{#if data.system.is_samurai}}
{{localize 'l5r5e.social.past'}}
{{else}}
{{localize 'l5r5e.social.giri'}} {{localize 'l5r5e.social.giri'}}
{{/ifCond}} {{else}}
<textarea type="text" name="system.social.giri" {{^if data.editable_not_soft_locked}}disabled{{/if}}>{{data.system.social.giri}}</textarea> {{localize 'chiaroscuro.character.past_problems'}}
{{/if}}
{{#if data.system.is_samurai}}
<textarea type="text" name="system.social.giri" {{^if data.editable_not_soft_locked}}disabled{{/if}}>{{data.system.social.giri}}</textarea>
{{else}}
<textarea type="text" name="system.social.past_problems" {{^if data.editable_not_soft_locked}}disabled{{/if}}>{{data.system.social.past_problems}}</textarea>
{{/if}}
</label> </label>
</fieldset> </fieldset>
{{!-- Bushido Tenets --}} {{!-- Bushido Tenets --}}

View File

@@ -2,35 +2,35 @@
<li id="earth"> <li id="earth">
<label class="earth {{#ifCond 'earth' '==' data.system.stance}}stance-active{{/ifCond}}"> <label class="earth {{#ifCond 'earth' '==' data.system.stance}}stance-active{{/ifCond}}">
<i class="i_earth dice-picker rollable" data-ring="earth"></i> <i class="i_earth dice-picker rollable" data-ring="earth"></i>
<strong>{{localizeRing 'earth'}}</strong> <strong class="ring-set-default {{#ifCond 'earth' '==' data.system.default_ring}}default-ring{{/ifCond}}" data-ring="earth" title="{{localize 'chiaroscuro.character.default_ring'}}">{{localizeRing 'earth'}}</strong>
<input class="centered-input select-on-focus" type="number" name="system.rings.earth" value="{{data.system.rings.earth}}" data-dtype="Number" min="1" max="9" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/> <input class="centered-input select-on-focus" type="number" name="system.rings.earth" value="{{data.system.rings.earth}}" data-dtype="Number" min="1" max="9" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
</label> </label>
</li> </li>
<li id="air"> <li id="air">
<label class="air {{#ifCond 'air' '==' data.system.stance}}stance-active{{/ifCond}}"> <label class="air {{#ifCond 'air' '==' data.system.stance}}stance-active{{/ifCond}}">
<i class="i_air dice-picker rollable" data-ring="air"></i> <i class="i_air dice-picker rollable" data-ring="air"></i>
<strong>{{localizeRing 'air'}}</strong> <strong class="ring-set-default {{#ifCond 'air' '==' data.system.default_ring}}default-ring{{/ifCond}}" data-ring="air" title="{{localize 'chiaroscuro.character.default_ring'}}">{{localizeRing 'air'}}</strong>
<input class="centered-input select-on-focus" type="number" name="system.rings.air" value="{{data.system.rings.air}}" data-dtype="Number" min="1" max="9" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/> <input class="centered-input select-on-focus" type="number" name="system.rings.air" value="{{data.system.rings.air}}" data-dtype="Number" min="1" max="9" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
</label> </label>
</li> </li>
<li id="water"> <li id="water">
<label class="water {{#ifCond 'water' '==' data.system.stance}}stance-active{{/ifCond}}"> <label class="water {{#ifCond 'water' '==' data.system.stance}}stance-active{{/ifCond}}">
<i class="i_water dice-picker rollable" data-ring="water"></i> <i class="i_water dice-picker rollable" data-ring="water"></i>
<strong>{{localizeRing 'water'}}</strong> <strong class="ring-set-default {{#ifCond 'water' '==' data.system.default_ring}}default-ring{{/ifCond}}" data-ring="water" title="{{localize 'chiaroscuro.character.default_ring'}}">{{localizeRing 'water'}}</strong>
<input class="centered-input select-on-focus" type="number" name="system.rings.water" value="{{data.system.rings.water}}" data-dtype="Number" min="1" max="9" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/> <input class="centered-input select-on-focus" type="number" name="system.rings.water" value="{{data.system.rings.water}}" data-dtype="Number" min="1" max="9" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
</label> </label>
</li> </li>
<li id="fire"> <li id="fire">
<label class="fire {{#ifCond 'fire' '==' data.system.stance}}stance-active{{/ifCond}}"> <label class="fire {{#ifCond 'fire' '==' data.system.stance}}stance-active{{/ifCond}}">
<i class="i_fire dice-picker rollable" data-ring="fire"></i> <i class="i_fire dice-picker rollable" data-ring="fire"></i>
<strong>{{localizeRing 'fire'}}</strong> <strong class="ring-set-default {{#ifCond 'fire' '==' data.system.default_ring}}default-ring{{/ifCond}}" data-ring="fire" title="{{localize 'chiaroscuro.character.default_ring'}}">{{localizeRing 'fire'}}</strong>
<input class="centered-input select-on-focus" type="number" name="system.rings.fire" value="{{data.system.rings.fire}}" data-dtype="Number" min="1" max="9" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/> <input class="centered-input select-on-focus" type="number" name="system.rings.fire" value="{{data.system.rings.fire}}" data-dtype="Number" min="1" max="9" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
</label> </label>
</li> </li>
<li id="void"> <li id="void">
<label class="void {{#ifCond 'void' '==' data.system.stance}}stance-active{{/ifCond}}"> <label class="void {{#ifCond 'void' '==' data.system.stance}}stance-active{{/ifCond}}">
<i class="i_void dice-picker rollable" data-ring="void"></i> <i class="i_void dice-picker rollable" data-ring="void"></i>
<strong>{{localizeRing 'void'}}</strong> <strong class="ring-set-default {{#ifCond 'void' '==' data.system.default_ring}}default-ring{{/ifCond}}" data-ring="void" title="{{localize 'chiaroscuro.character.default_ring'}}">{{localizeRing 'void'}}</strong>
<input class="centered-input select-on-focus" type="number" name="system.rings.void" value="{{data.system.rings.void}}" data-dtype="Number" min="1" max="9" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/> <input class="centered-input select-on-focus" type="number" name="system.rings.void" value="{{data.system.rings.void}}" data-dtype="Number" min="1" max="9" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
</label> </label>
</li> </li>

View File

@@ -1,16 +1,9 @@
<li class="skill skill-wrapper"> <li class="skill skill-wrapper">
<label class="skill-content"> <label class="skill-content">
<span class="dice-picker attribute-label rollable" data-skill="{{skillId}}">{{localizeSkill categoryId skillId}}</span> <span class="dice-picker attribute-label rollable" data-skill="{{skillId}}">{{localizeSkill categoryId skillId}}</span>
<input <select name="system.skills.{{categoryId}}.{{skillId}}" {{^if data.editable_not_soft_locked}}disabled{{/if}}>
class="centered-input select-on-focus" {{selectOptions data.skillRanksList selected=skill valueAttr='id' labelAttr='label'}}
type="number" </select>
name="system.skills.{{categoryId}}.{{skillId}}" {{#ifCond skill '!=' '0'}}<span class="skill-bonus">+{{skillRankBonus skill}}</span>{{/ifCond}}
value="{{skill}}"
data-dtype="Number"
min="0"
max="9"
placeholder="0"
{{^if data.editable_not_soft_locked}}disabled{{/if}}
/>
</label> </label>
</li> </li>

View File

@@ -2,18 +2,30 @@
<li> <li>
<label class="attribute-label centered-input"> <label class="attribute-label centered-input">
<strong>{{localize 'l5r5e.social.honor'}}</strong> <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}}/> <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> </label>
</li> </li>
<li> <li>
<label class="attribute-label centered-input"> <label class="attribute-label centered-input">
<strong>{{localize 'l5r5e.social.glory'}}</strong> <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}}/> <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> </label>
</li> </li>
<li> <li>
<label class="attribute-label centered-input"> <label class="attribute-label centered-input">
<strong>{{localize 'l5r5e.social.status'}}</strong> <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}}/> <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> </label>
</li> </li>

View File

@@ -45,4 +45,27 @@
{{/each}} {{/each}}
</ul> </ul>
</fieldset> </fieldset>
{{!-- Arcane items list --}}
<fieldset class="section-header flexrow">
<legend class="text-block-header">
{{localize 'chiaroscuro.arcane.title'}}
{{#if data.editable_not_soft_locked}}
<a data-item-type="arcane" class="arcane-control item-add" title="{{localize 'l5r5e.global.add'}}"><i class="fas fa-plus"></i></a>
{{/if}}
</legend>
<ul class="item-list">
{{#each data.arcaneItems as |item|}}
<li class="item technique flexcol" data-item-id="{{item._id}}">
<ul class="item-header technique-controls">
<li class="item-img"><img src="{{item.img}}" title="{{item.name}}" width="32px" height="32px"/></li>
<li class="item-name l5r5e-tooltip" data-item-id="{{item._id}}">{{item.name}}</li>
{{#if ../data.editable_not_soft_locked}}
<li data-item-id="{{item._id}}" class="item-control item-edit" title="{{localize 'l5r5e.global.edit'}}"><i class="fas fa-edit"></i></li>
<li data-item-id="{{item._id}}" class="item-control item-delete" title="{{localize 'Delete'}}"><i class="fas fa-trash"></i></li>
{{/if}}
</ul>
</li>
{{/each}}
</ul>
</fieldset>
</div> </div>

View File

@@ -41,6 +41,7 @@
<nav class="sheet-tabs tabs" data-group="primary"> <nav class="sheet-tabs tabs" data-group="primary">
<a class="item" data-tab="skills">{{localize 'l5r5e.skills.title'}}</a> <a class="item" data-tab="skills">{{localize 'l5r5e.skills.title'}}</a>
<a class="item" data-tab="narrative">{{localize 'l5r5e.sheets.narrative'}}</a> <a class="item" data-tab="narrative">{{localize 'l5r5e.sheets.narrative'}}</a>
<a class="item" data-tab="invocations">{{localize 'chiaroscuro.tabs.invocations'}}</a>
<a class="item" data-tab="conflict">{{localize 'l5r5e.conflict.title'}}</a> <a class="item" data-tab="conflict">{{localize 'l5r5e.conflict.title'}}</a>
<a class="item" data-tab="inventory">{{localize 'l5r5e.sheets.inventory'}}</a> <a class="item" data-tab="inventory">{{localize 'l5r5e.sheets.inventory'}}</a>
</nav> </nav>
@@ -55,6 +56,11 @@
{{> 'systems/l5r5e/templates/actors/npc/narrative.html'}} {{> 'systems/l5r5e/templates/actors/npc/narrative.html'}}
</article> </article>
{{!-- Invocations Tab --}}
<article class="tab invocations" data-group="primary" data-tab="invocations">
{{> 'systems/l5r5e/templates/actors/character/invocations.html'}}
</article>
{{!-- Conflict Tab --}} {{!-- Conflict Tab --}}
<article class="tab conflict" data-group="primary" data-tab="conflict"> <article class="tab conflict" data-group="primary" data-tab="conflict">
{{> 'systems/l5r5e/templates/actors/npc/conflict.html'}} {{> {{> 'systems/l5r5e/templates/actors/npc/conflict.html'}} {{>

View File

@@ -7,7 +7,7 @@
<label class="attribute-label"> <label class="attribute-label">
<strong>{{localize 'l5r5e.attributes.fatigue'}}</strong> <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}}/> <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-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> <i class="addsub-control pointer-choice fa fa-minus-square" data-type="fatigue" data-value="-1"></i>
</span> </span>
@@ -22,7 +22,7 @@
<label class="attribute-label"> <label class="attribute-label">
<strong>{{localize 'l5r5e.attributes.strife'}}</strong> <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}}/> <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-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> <i class="addsub-control pointer-choice fa fa-minus-square" data-type="strife" data-value="-1"></i>
</span> </span>

View File

@@ -5,14 +5,34 @@
{{selectOptions data.types selected=data.system.type valueAttr='id' labelAttr='label'}} {{selectOptions data.types selected=data.system.type valueAttr='id' labelAttr='label'}}
</select> </select>
</li> </li>
{{!-- Martial --}} {{!-- Martial Danger --}}
<li> <li class="danger-row">
<i class="i_bushi" title="{{localize 'l5r5e.social.npc.combat'}}"></i> <i class="i_bushi" title="{{localize 'l5r5e.social.npc.combat'}}"></i>
<input class="centered-input select-on-focus" type="number" name="system.conflict_rank.martial" value="{{data.system.conflict_rank.martial}}" data-dtype="Number" min="0" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/> <div class="danger-wrapper">
<select name="system.martial_danger" {{^if data.editable_not_soft_locked}}disabled{{/if}}>
{{selectOptions data.dangerList selected=data.system.martial_danger valueAttr='id' labelAttr='label'}}
</select>
<div class="danger-icons">
<i class="fas fa-skull danger-icon"></i>
{{#ifCond '["moyenne","assez_difficile","difficile"]' 'includes' data.system.martial_danger}}<i class="fas fa-skull danger-icon"></i>{{/ifCond}}
{{#ifCond '["assez_difficile","difficile"]' 'includes' data.system.martial_danger}}<i class="fas fa-skull danger-icon"></i>{{/ifCond}}
{{#ifCond data.system.martial_danger '==' 'difficile'}}<i class="fas fa-skull danger-icon"></i>{{/ifCond}}
</div>
</div>
</li> </li>
{{!-- Social --}} {{!-- Social Danger --}}
<li> <li class="danger-row">
<i class="i_courtier" title="{{localize 'l5r5e.social.npc.intrigue'}}"></i> <i class="i_courtier" title="{{localize 'l5r5e.social.npc.intrigue'}}"></i>
<input class="centered-input select-on-focus" type="number" name="system.conflict_rank.social" value="{{data.system.conflict_rank.social}}" data-dtype="Number" min="0" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/> <div class="danger-wrapper">
<select name="system.social_danger" {{^if data.editable_not_soft_locked}}disabled{{/if}}>
{{selectOptions data.dangerList selected=data.system.social_danger valueAttr='id' labelAttr='label'}}
</select>
<div class="danger-icons">
<i class="fas fa-star danger-icon"></i>
{{#ifCond '["moyenne","assez_difficile","difficile"]' 'includes' data.system.social_danger}}<i class="fas fa-star danger-icon"></i>{{/ifCond}}
{{#ifCond '["assez_difficile","difficile"]' 'includes' data.system.social_danger}}<i class="fas fa-star danger-icon"></i>{{/ifCond}}
{{#ifCond data.system.social_danger '==' 'difficile'}}<i class="fas fa-star danger-icon"></i>{{/ifCond}}
</div>
</div>
</li> </li>
</ul> </ul>

View File

@@ -2,18 +2,30 @@
<li> <li>
<label class="attribute-label centered-input"> <label class="attribute-label centered-input">
<strong>{{localize 'l5r5e.social.honor'}}</strong> <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}}/> <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> </label>
</li> </li>
<li> <li>
<label class="attribute-label centered-input"> <label class="attribute-label centered-input">
<strong>{{localize 'l5r5e.social.glory'}}</strong> <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}}/> <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> </label>
</li> </li>
<li> <li>
<label class="attribute-label centered-input"> <label class="attribute-label centered-input">
<strong>{{localize 'l5r5e.social.status'}}</strong> <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}}/> <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> </label>
</li> </li>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,52 @@
<div class="l5r5e chat-roll chiaroscuro-chat-roll">
{{!-- Header --}}
<div class="chi-chat-header">
<img class="profile-img" src="{{profileImg}}" alt="{{actor.name}}" />
<div class="chi-chat-actor">
<strong>{{actor.name}}</strong>
{{#if quickInfo}}<div class="chi-chat-quick-info">{{quickInfo}}</div>{{/if}}
</div>
<div class="chi-chat-badges">
<span class="ring-icon {{ring.id}}" title="{{ring.label}}"><i class="i_{{ring.id}}"></i></span>
{{#if useAspectPoint}}<span class="chi-aspect-badge {{aspectType}}" title="{{localize 'chiaroscuro.dice.aspect_point'}}">
{{#ifCond aspectType '==' 'solar'}}☀{{else}}☽{{/ifCond}}
</span>{{/if}}
{{#if useAssistance}}<span class="chi-assistance-badge" title="{{localize 'chiaroscuro.dice.assistance'}}"></span>{{/if}}
</div>
</div>
{{!-- Roll description line --}}
<div class="chi-chat-desc">
{{#if skill.name}}<span class="chi-chat-skill">{{skill.name}}</span>{{/if}}
<span class="chi-chat-vs"></span>
<span class="chi-chat-diff">{{localize (concat 'chiaroscuro.difficulties.' difficulty.id)}} ({{difficulty.value}})</span>
</div>
{{!-- Dice results --}}
<div class="chi-chat-dice-pool">
{{#each adjustedResults}}
<span class="chi-die {{#ifCond this '<' 3}}die-low{{else}}{{#ifCond this '>' 4}}die-high{{/ifCond}}{{/ifCond}}">
{{this}}
{{#if (lookup ../diceAdjustedFlags @index)}}<i class="fas fa-caret-up die-adj-icon" title="{{localize 'chiaroscuro.dice.adjusted'}}"></i>{{/if}}
</span>
{{/each}}
</div>
{{!-- Breakdown --}}
<div class="chi-chat-breakdown">
<span>{{localize 'chiaroscuro.dice.dice_result'}} : {{rawSum}}</span>
{{#if skill.bonus}}<span> + {{skill.bonus}} ({{localize 'chiaroscuro.dice.bonus'}})</span>{{/if}}
{{#if modifier}}<span> {{#ifCond modifier '>' 0}}+{{/ifCond}}{{modifier}} ({{localize 'chiaroscuro.dice.modifier_label'}})</span>{{/if}}
<strong> = {{total}}</strong>
</div>
{{!-- Result --}}
<div class="chi-chat-result {{#if success}}chi-success{{else}}chi-failure{{/if}}">
{{#if success}}
<i class="fas fa-check"></i> {{localize 'chiaroscuro.dice.success'}}
{{#if bonus}}<span class="chi-bonus-successes">(+{{bonus}})</span>{{/if}}
{{else}}
<i class="fas fa-times"></i> {{localize 'chiaroscuro.dice.failure'}}
{{/if}}
</div>
</div>

View File

@@ -0,0 +1,97 @@
<form class="l5r5e chiaroscuro-dice-dialog" autocomplete="off">
{{!-- Header: portrait + quick info --}}
<div class="chi-dice-header">
<img class="profile-img" src="{{#if actor.img}}{{actor.img}}{{else}}icons/svg/mystery-man.svg{{/if}}" alt="{{actor.name}}" />
<div class="chi-dice-actor-info">
<strong>{{actor.name}}</strong>
{{#if quickInfo}}<div class="chi-dice-quick-info">{{quickInfo}}</div>{{/if}}
</div>
</div>
{{!-- Ring selector --}}
<fieldset class="chi-dice-section">
<legend>{{localize 'l5r5e.rings.title'}}</legend>
<ul class="rings chi-rings">
{{#each ringsList}}
<li>
<label class="attribute-label {{this.id}} centered-input ring-selection-chi pointer-choice {{#ifCond ../data.ring.id '==' this.id}}ring-selected{{/ifCond}}" data-ringid="{{this.id}}">
<i class="i_{{this.id}}"></i>
<strong>{{this.label}}</strong>
<span class="ring-value">{{this.value}}</span>
</label>
</li>
{{/each}}
</ul>
</fieldset>
{{!-- Skill info --}}
{{#if data.skill.name}}
<fieldset class="chi-dice-section">
<legend>{{localize 'l5r5e.skills.title'}}</legend>
<div class="chi-skill-row">
<span class="chi-skill-name">{{data.skill.name}}</span>
<span class="chi-skill-rank">{{localize (concat 'chiaroscuro.skill_ranks.' data.skill.rank)}}</span>
{{#ifCond data.skill.bonus '>' 0}}<span class="chi-skill-bonus skill-bonus">+{{data.skill.bonus}}</span>{{/ifCond}}
</div>
</fieldset>
{{/if}}
{{!-- Difficulty + Modifier --}}
<fieldset class="chi-dice-section">
<legend>{{localize 'chiaroscuro.dice.difficulty_label'}}</legend>
<div class="chi-difficulty-row">
<select name="difficulty.id">
{{selectOptions difficultiesList selected=data.difficulty.id valueAttr='id' labelAttr='label'}}
</select>
<label class="chi-modifier-label">
{{localize 'chiaroscuro.dice.modifier_label'}}
<input type="number" name="modifier" value="{{data.modifier}}" class="centered-input select-on-focus" style="width: 3em;" />
</label>
</div>
</fieldset>
{{!-- Aspect Point + Assistance --}}
<fieldset class="chi-dice-section">
<legend>{{localize 'chiaroscuro.dice.options'}}</legend>
<div class="chi-options-row">
<label>
<input type="checkbox" id="use_aspect_point" {{checked data.useAspectPoint}} />
{{localize 'chiaroscuro.dice.aspect_point'}}
{{#if isVoidRing}}
<select name="aspectType">
{{selectOptions aspectsList selected=data.aspectType valueAttr='id' labelAttr='label'}}
</select>
{{else}}
<em class="chi-auto-aspect">
{{#ifCond '["fire","earth"]' 'includes' data.ring.id}}
({{localize 'chiaroscuro.aspects.solar'}})
{{else}}
({{localize 'chiaroscuro.aspects.lunar'}})
{{/ifCond}}
</em>
{{/if}}
</label>
</div>
<div class="chi-options-row">
<label>
<input type="checkbox" id="use_assistance" {{checked data.useAssistance}} />
{{localize 'chiaroscuro.dice.assistance'}}
</label>
</div>
</fieldset>
{{!-- Total dice summary --}}
<div class="chi-dice-total-summary">
<span>{{localize 'chiaroscuro.dice.total_dice'}} :</span>
<strong class="chi-total-dice">{{totalDice}}d6</strong>
{{#if data.skill.bonus}}<span> + {{data.skill.bonus}} ({{localize 'chiaroscuro.dice.bonus'}})</span>{{/if}}
{{#if data.modifier}}<span> {{#ifCond data.modifier '>' 0}}+{{/ifCond}}{{data.modifier}} ({{localize 'chiaroscuro.dice.modifier_label'}})</span>{{/if}}
</div>
{{!-- Submit --}}
<div class="chi-dice-submit">
<button name="roll" type="submit">
{{localize 'chiaroscuro.dice.roll'}} <i class="fas fa-dice"></i>
</button>
</div>
</form>

View File

@@ -1,4 +1,5 @@
<form class="l5r5e dice-picker-dialog" autocomplete="off"> <form class="l5r5e dice-picker-dialog" autocomplete="off">
{{> 'systems/l5r5e/templates/actors/character/effects.html'}}
<table> <table>
{{!-- First line--}} {{!-- First line--}}
<tr> <tr>

View File

@@ -35,6 +35,7 @@
{{/if}} {{/if}}
</li> </li>
</ul> </ul>
{{> 'systems/l5r5e/templates/actors/character/effects.html' actor=l5r5e.actor}}
</header> </header>
<section class="rnk-ct"> <section class="rnk-ct">
{{!-- Body --}} {{!-- Body --}}

View File

@@ -1,19 +1,19 @@
<form class="l5r5e gm-toolbox" autocomplete="off"> <form class="l5r5e gm-toolbox" autocomplete="off">
<ul class="gm-tools-container"> <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> <i class="fas fa-table"></i>
</li> </li>
<li class="difficulty_hidden" data-action="toggle_hide_difficulty" title="{{localize 'l5r5e.gm.toolbox.difficulty_hidden'}}"> <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> <i class="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> <strong class="difficulty" data-action="change_difficulty" title="{{localize 'l5r5e.gm.toolbox.difficulty'}}">{{difficulty}}</strong>
</li> </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> <i class="fas fa-podcast"></i>
</li> </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> <i class="fa fa-bed"></i>
</li> </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> <i class="fas fa-star-half-alt"></i>
</li> </li>
</ul> </ul>

View 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>

View File

@@ -0,0 +1,35 @@
<form class="{{cssClass}}" autocomplete="off">
<header class="sheet-header">
<img class="profile-img" src="{{data.img}}" data-edit="img" title="{{data.name}}"/>
<h1 class="charname"><input name="name" type="text" value="{{data.name}}" placeholder="Name"/></h1>
</header>
<nav class="sheet-tabs tabs" data-group="primary">
<a class="item active" data-group="primary" data-tab="attributes">{{localize 'l5r5e.sheets.attributes'}}</a>
<a class="item" data-group="primary" data-tab="description">{{localize 'l5r5e.sheets.infos'}}</a>
</nav>
<section class="sheet-body">
<article class="attributes" data-group="primary" data-tab="attributes">
<label class="attribute">
{{localize 'chiaroscuro.arcane.arcane_type'}}
<input type="text" name="system.arcane_type" value="{{data.system.arcane_type}}" />
</label>
<label class="attribute">
{{localize 'chiaroscuro.arcane.application'}}
<input type="text" name="system.applicationDisplay" value="{{data.system.applicationDisplay}}" placeholder="compétence1, compétence2" />
</label>
<label class="attribute">
{{localize 'chiaroscuro.arcane.bonus'}}
<input class="select-on-focus" type="number" name="system.bonus" value="{{data.system.bonus}}" data-dtype="Number" min="0" placeholder="2"/>
</label>
<label class="attribute">
{{localize 'chiaroscuro.arcane.progression'}}
<input type="text" name="system.progression" value="{{data.system.progression}}" />
</label>
<label class="attribute">
{{localize 'chiaroscuro.arcane.xp_cost'}}
<input class="select-on-focus" type="number" name="system.xp_cost" value="{{data.system.xp_cost}}" data-dtype="Number" min="0" placeholder="1"/>
</label>
</article>
{{> 'systems/l5r5e/templates/items/item/item-infos.html'}}
</section>
</form>

View File

@@ -11,6 +11,13 @@
<input type="checkbox" name="system.equipped" {{checked data.system.equipped}} /> <input type="checkbox" name="system.equipped" {{checked data.system.equipped}} />
{{ localize 'l5r5e.armors.equipped' }} {{ localize 'l5r5e.armors.equipped' }}
</label> </label>
<label class="attribute">
{{localize 'l5r5e.armors.type'}}
<select name="system.armor_category">
<option value=""></option>
{{selectOptions data.armorCategories selected=data.system.armor_category valueAttr='id' labelAttr='label'}}
</select>
</label>
{{> 'systems/l5r5e/templates/items/item/item-value.html' }} {{> 'systems/l5r5e/templates/items/item/item-value.html' }}
<fieldset class="attribute type"> <fieldset class="attribute type">
<legend class="text-header">{{localize 'l5r5e.armors.type'}}</legend> <legend class="text-header">{{localize 'l5r5e.armors.type'}}</legend>

View File

@@ -0,0 +1,31 @@
<form class="{{cssClass}}" autocomplete="off">
<header class="sheet-header">
<img class="profile-img" src="{{data.img}}" data-edit="img" title="{{data.name}}"/>
<h1 class="charname"><input name="name" type="text" value="{{data.name}}" placeholder="Name"/></h1>
</header>
<nav class="sheet-tabs tabs" data-group="primary">
<a class="item active" data-group="primary" data-tab="attributes">{{localize 'l5r5e.sheets.attributes'}}</a>
<a class="item" data-group="primary" data-tab="description">{{localize 'l5r5e.sheets.infos'}}</a>
</nav>
<section class="sheet-body">
<article class="attributes" data-group="primary" data-tab="attributes">
<label class="attribute">
{{localize 'chiaroscuro.etat.application'}}
<input type="text" name="system.application" value="{{data.system.application}}" />
</label>
<label class="attribute">
{{localize 'chiaroscuro.etat.mod'}}
<input class="select-on-focus" type="number" name="system.mod" value="{{data.system.mod}}" data-dtype="Number" placeholder="0"/>
</label>
<label class="attribute">
{{localize 'chiaroscuro.etat.effect'}}
<input type="text" name="system.effect" value="{{data.system.effect}}" />
</label>
<label class="attribute">
{{localize 'chiaroscuro.etat.elimination'}}
<input type="text" name="system.elimination" value="{{data.system.elimination}}" />
</label>
</article>
{{> 'systems/l5r5e/templates/items/item/item-infos.html'}}
</section>
</form>

View File

@@ -0,0 +1,29 @@
<form class="{{cssClass}}" autocomplete="off">
<header class="sheet-header">
<img class="profile-img" src="{{data.img}}" data-edit="img" title="{{data.name}}"/>
<h1 class="charname"><input name="name" type="text" value="{{data.name}}" placeholder="Name"/></h1>
</header>
<nav class="sheet-tabs tabs" data-group="primary">
<a class="item active" data-group="primary" data-tab="attributes">{{localize 'l5r5e.sheets.attributes'}}</a>
<a class="item" data-group="primary" data-tab="description">{{localize 'l5r5e.sheets.infos'}}</a>
</nav>
<section class="sheet-body">
<article class="attributes" data-group="primary" data-tab="attributes">
<label class="attribute">
{{localize 'chiaroscuro.mystere.mystere_type'}}
<select name="system.mystere_type">
{{selectOptions data.mystereTypes selected=data.system.mystere_type valueAttr='id' labelAttr='label'}}
</select>
</label>
<label class="attribute">
{{localize 'chiaroscuro.mystere.prerequisite_skill'}}
<input type="text" name="system.prerequisite_skill" value="{{data.system.prerequisite_skill}}" />
</label>
<label class="attribute">
{{localize 'chiaroscuro.mystere.prerequisite_condition'}}
<input type="text" name="system.prerequisite_condition" value="{{data.system.prerequisite_condition}}" />
</label>
</article>
{{> 'systems/l5r5e/templates/items/item/item-infos.html'}}
</section>
</form>

View File

@@ -42,6 +42,20 @@
{{localize 'l5r5e.dice.dicepicker.difficulty_title'}} {{localize 'l5r5e.dice.dicepicker.difficulty_title'}}
<input class="select-on-focus" type="text" name="system.difficulty" value="{{data.system.difficulty}}" data-dtype="String"/> <input class="select-on-focus" type="text" name="system.difficulty" value="{{data.system.difficulty}}" data-dtype="String"/>
</label> </label>
{{#if data.isMotInvocation}}
<label class="attribute">
{{localize 'chiaroscuro.technique.invocation_type'}}
<select name="system.invocation_type">
{{selectOptions data.invocationTypes selected=data.system.invocation_type valueAttr='id' labelAttr='label'}}
</select>
</label>
<label class="attribute">
{{localize 'chiaroscuro.technique.mode_invocation'}}
<select name="system.mode_invocation_str">
{{selectOptions data.modeInvocationValues selected=data.system.mode_invocation_str valueAttr='id' labelAttr='label'}}
</select>
</label>
{{/if}}
</article> </article>
{{> 'systems/l5r5e/templates/items/item/item-infos.html'}} {{> 'systems/l5r5e/templates/items/item/item-infos.html'}}
</section> </section>

View File

@@ -18,7 +18,13 @@
{{> 'systems/l5r5e/templates/items/item/item-value.html'}} {{> 'systems/l5r5e/templates/items/item/item-value.html'}}
<label class="category"> <label class="category">
{{localize 'l5r5e.weapons.category'}} {{localize 'l5r5e.weapons.category'}}
<input type="text" name="system.category" value="{{data.system.category}}" /> <select name="system.category">
{{selectOptions data.weaponCategories selected=data.system.category valueAttr='id' labelAttr='label'}}
</select>
</label>
<label class="attribute">
{{localize 'chiaroscuro.weapon.bonus'}}
<input class="select-on-focus" type="number" name="system.bonus" value="{{data.system.bonus}}" data-dtype="Number" placeholder="0"/>
</label> </label>
<label class="skillType"> <label class="skillType">
{{localize 'l5r5e.skills.label'}} {{localize 'l5r5e.skills.label'}}

View 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>

View File

@@ -1,7 +1,9 @@
# DicePicker (DP) # DicePicker (DP)
The DicePicker is the entry point to any L5R roll (but chat command). The DicePicker is the entry point to any L5R roll (but chat command).
## Usage example ## Usage example
```js ```js
new game.l5r5e.DicePickerDialog({ new game.l5r5e.DicePickerDialog({
skillId: 'aesthetics', skillId: 'aesthetics',
@@ -11,26 +13,27 @@ new game.l5r5e.DicePickerDialog({
``` ```
## Constructor Options ## 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" |
| difficulty | number | `1` to `9` |
| difficultyHidden | boolean | If `true`, hide the difficulty and lock the view for the player. |
| isInitiativeRoll | boolean | `true` if this is an initiative roll. |
| 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" |
| 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_ |
| 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" |
| difficulty | number | `1` to `9` |
| difficultyHidden | boolean | If `true`, hide the difficulty and lock the view for the player. |
| isInitiativeRoll | boolean | `true` if this is an initiative roll. |
| 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" |
| 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. All these properties are optional.
For `actor*` properties, the resolution is in this order : For `actor*` properties, the resolution is in this order :
1. `option.actor` 1. `option.actor`
2. `option.actorId` 2. `option.actorId`
3. `option.actorName` 3. `option.actorName`

View File

@@ -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. > If you have any idea how to modify directly the ChatMessage and update it, let me know.
Usage : Usage :
```js ```js
new RollnKeepDialog(messageId).render(true); new RollnKeepDialog(messageId).render(true);
``` ```

View File

@@ -1,7 +1,9 @@
# Roll # Roll
The roll use the `RollL5r5e` class, who store a lot of additional variables. 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 ```js
actor: null, // actor instance actor: null, // actor instance
dicesTypes: { dicesTypes: {

View File

@@ -1,10 +1,13 @@
# Snippets # 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 ```js
// By uuid // By uuid
actor = fromUuid("Actor.lQYEzLeDS5ndopJV"); // or `fromUuidSync() actor = fromUuid("Actor.lQYEzLeDS5ndopJV"); // or `fromUuidSync()
@@ -28,27 +31,30 @@ actors = Array.from(game.user.targets).map(t => t.actor);
actor = game.user.character; actor = game.user.character;
``` ```
### Document open sheet
## Document open sheet
```js ```js
<document>.sheet?.render(true); <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 ```js
// url, options // url, options
new FrameViewer("https://foundryvtt.wiki/", { title: "SIDEBAR.Wiki" }).render(true); new FrameViewer("https://foundryvtt.wiki/", { title: "SIDEBAR.Wiki" }).render(true);
``` ```
## L5R5e specific ## L5R5e specific
### autocomplete ### autocomplete
A basic autocomplete for text inputs A basic autocomplete for text inputs
Parameters : Parameters :
```
```md
@param {jQuery} html HTML content of the sheet. @param {jQuery} html HTML content of the sheet.
@param {string} name Html name of the input @param {string} name Html name of the input
@param {string[]} list Array of string to display @param {string[]} list Array of string to display
@@ -56,6 +62,7 @@ Parameters :
``` ```
Usage examples : Usage examples :
```js ```js
game.l5r5e.HelpersL5r5e.autocomplete( game.l5r5e.HelpersL5r5e.autocomplete(
html, html,
@@ -70,17 +77,19 @@ game.l5r5e.HelpersL5r5e.autocomplete(
``` ```
It produces two values that can be useful in some cases : It produces two values that can be useful in some cases :
```js ```js
formData["autoCompleteListName"]; // "system.difficulty" formData["autoCompleteListName"]; // "system.difficulty"
formData["autoCompleteListSelectedIndex"]; // 0 <- 1st élément selected formData["autoCompleteListSelectedIndex"]; // 0 <- 1st element selected
``` ```
### Debounce
### debounce
Isolated Debounce by Id Isolated Debounce by Id
Parameters : Parameters :
```
```md
@param {String} id Named id (namespace) @param {String} id Named id (namespace)
@param {Function} callback Callback function @param {Function} callback Callback function
@param {Number} timeout Wait time (default 500ms) @param {Number} timeout Wait time (default 500ms)
@@ -89,6 +98,7 @@ Parameters :
``` ```
Usage examples : Usage examples :
```js ```js
// Basic usage, non leading // Basic usage, non leading
game.l5r5e.HelpersL5r5e.debounce('appId', (text) => { console.log(text) })('my text'); game.l5r5e.HelpersL5r5e.debounce('appId', (text) => { console.log(text) })('my text');
@@ -102,12 +112,13 @@ game.l5r5e.HelpersL5r5e.debounce(
)(); )();
``` ```
### drawManyFromPack ### drawManyFromPack
Shortcut method to draw names to chat (private) from a table in compendium without importing it Shortcut method to draw names to chat (private) from a table in compendium without importing it
Parameters : Parameters :
```
```md
@param {String} pack Compendium name @param {String} pack Compendium name
@param {String} tableName Table name/id in this compendium @param {String} tableName Table name/id in this compendium
@param {String} retrieve How many draw we do (default "5") @param {String} retrieve How many draw we do (default "5")
@@ -115,24 +126,28 @@ Parameters :
@return {Promise<{RollTableDraw}>} The drawn results @return {Promise<{RollTableDraw}>} The drawn results
``` ```
Usage examples : Usage examples:
```js ```js
game.l5r5e.HelpersL5r5e.drawManyFromPack("l5r5e.core-name-tables", "Japanese names (Village)", 5); game.l5r5e.HelpersL5r5e.drawManyFromPack("l5r5e.core-name-tables", "Japanese names (Village)", 5);
``` ```
### migrateWorld ### migrateWorld
You can force to trigger the system migration by using : You can force to trigger the system migration by using :
```js ```js
game.l5r5e.migrations.migrateWorld({force: true}); game.l5r5e.migrations.migrateWorld({force: true});
``` ```
This will try to normalize the actor/items in the current loaded world. This will try to normalize the actor/items in the current loaded world.
### sendToChat ### sendToChat
Send the description of this `Document` (`BaseSheetL5r5e`, `JournalL5r5e`, `ItemL5r5e`) to chat. Send the description of this `Document` (`BaseSheetL5r5e`, `JournalL5r5e`, `ItemL5r5e`) to chat.
Usage examples : Usage examples :
```js ```js
game.l5r5e.HelpersL5r5e.sendToChat(game.actors.getName("Soshi Yui")); game.l5r5e.HelpersL5r5e.sendToChat(game.actors.getName("Soshi Yui"));
``` ```

View File

@@ -1,52 +1,56 @@
# Sockets API # 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 ## 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 : Usage :
```js ```js
game.l5r5e.sockets.deleteChatMessage(messageId); game.l5r5e.sockets.deleteChatMessage(messageId);
``` ```
## refreshAppId ## refreshAppId
Refresh an application windows by his `id` (not `appId`). Used in RnK. Refresh an application windows by his `id` (not `appId`). Used in RnK.
<br>Ex : `l5r5e-twenty-questions-dialog-kZHczAFghMNYFRWe`, not `65`. <br>Ex : `l5r5e-twenty-questions-dialog-kZHczAFghMNYFRWe`, not `65`.
Usage : Usage :
```js ```js
game.l5r5e.sockets.refreshAppId(applicationId); game.l5r5e.sockets.refreshAppId(applicationId);
``` ```
## updateMessageIdAndRefresh ## updateMessageIdAndRefresh
Change the message in the selected application windows, and re-render the application to force the refresh. Used in RnK. Change the message in the selected application windows, and re-render the application to force the refresh. Used in RnK.
Usage : Usage :
```js ```js
game.l5r5e.sockets.refreshAppId(applicationId, messageId); game.l5r5e.sockets.refreshAppId(applicationId, messageId);
``` ```
## openDicePicker ## openDicePicker
_Added in v1.9.0_ _Added in v1.9.0_
Remotely open the DicePicker (DP) on targeted Users/Actors if they are active users. Used in initiative roll. Remotely open the DicePicker (DP) on targeted Users/Actors if they are active users. Used in initiative roll.
Arguments : Arguments :
| Property | Type | Notes / Examples | | Property | Type | Notes / Examples |
|-----------|---------|----------------------------------------------------------------------------| |:----------|:--------|:-----------------|
| users | User[] | Users list to trigger the DP (will be reduce to `id` for network perf.) | | 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.) | | 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) | | dpOptions | Object | Any [DicePickerDialog.options](dicepicker.md#constructor-options) |
### Examples ### Examples
#### Fitness skill roll for the all combat targets #### Fitness skill roll for the all combat targets
```js ```js
game.l5r5e.sockets.openDicePicker({ game.l5r5e.sockets.openDicePicker({
actors: Array.from(game.user.targets).map(t => t.document.actor), 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 #### Initiative roll (skirmish) for all player's character who are in combat tracker
```js ```js
game.l5r5e.sockets.openDicePicker({ game.l5r5e.sockets.openDicePicker({
actors: game.combat.combatants.filter(c => c.hasPlayerOwner && !c.isDefeated && !c.initiative).map(c => c.actor), 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 #### Melee skill roll with "fire" ring, pre-selected for all the selected tokens
```js ```js
game.l5r5e.sockets.openDicePicker({ game.l5r5e.sockets.openDicePicker({
actors: canvas.tokens.controlled.map(t => t.actor), 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) #### Skill roll with skill list for all active players (but GM)
```js ```js
game.l5r5e.sockets.openDicePicker({ game.l5r5e.sockets.openDicePicker({
users: game.users.players.filter(u => u.active && u.hasPlayerOwner), users: game.users.players.filter(u => u.active && u.hasPlayerOwner),

View File

@@ -1,4 +1,5 @@
# Storage API # Storage API
Client side volatile storage - Store things like collapsible state (a refresh will clean it). Client side volatile storage - Store things like collapsible state (a refresh will clean it).
This is accessible anytime on `game.l5r5e.storage`. This is accessible anytime on `game.l5r5e.storage`.
@@ -6,14 +7,17 @@ Used in sheets to store some collapsible element state.
## getAppKeys ## getAppKeys
Get list of active keys for this app Get list of active keys for this app
Parameters : Parameters :
```
```js
@param {string} app application name @param {string} app application name
``` ```
Usage examples : Usage examples :
```js ```js
storeInfos = game.l5r5e.storage.getAppKeys("my-appid-namespace"); storeInfos = game.l5r5e.storage.getAppKeys("my-appid-namespace");
@@ -25,19 +29,23 @@ storeInfos = game.l5r5e.storage.getAppKeys("CharacterSheetL5r5e-Actor-Zca44Nv7yd
// 'inventory-item-list-weapon' // 'inventory-item-list-weapon'
// ] // ]
``` ```
A defined key is "active", else they won't appear. A defined key is "active", else they won't appear.
## toggleKey ## toggleKey
Toggle a key for this app. Toggle a key for this app.
Parameters : Parameters :
```
```js
@param {string} app application name @param {string} app application name
@param {string} key Key name @param {string} key Key name
``` ```
Usage examples : Usage examples :
```js ```js
game.l5r5e.storage.toggleKey("my-appid-namespace", "var-key-to-toggle"); game.l5r5e.storage.toggleKey("my-appid-namespace", "var-key-to-toggle");

Some files were not shown because too many files have changed in this diff Show More