Compare commits

...

27 Commits

Author SHA1 Message Date
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
64 changed files with 3259 additions and 1025 deletions

View File

@@ -6,6 +6,20 @@ Date format : day/month/year
> - `foundry-version`: Stick to the major version of FoundryVTT.
> - `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).

View File

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

View File

@@ -8,7 +8,7 @@
Created by potrace 1.15, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,1264.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
fill="currentColor" stroke="none">
<path d="M5925 12625 c-248 -21 -380 -39 -378 -51 1 -7 -56 -18 -148 -29 -83
-9 -190 -25 -237 -35 -244 -53 -454 -105 -602 -151 -91 -28 -210 -64 -264 -81
-465 -138 -1038 -415 -1493 -719 -234 -157 -585 -429 -773 -600 -215 -196

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -1,13 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="48.2px" height="48.2px" viewBox="0 0 48.2 48.2" style="enable-background:new 0 0 48.2 48.2;" xml:space="preserve">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="48.2px" height="48.2px" viewBox="0 0 48.2 48.2" xml:space="preserve">
<defs>
<linearGradient id="iconGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#E0E0E0;stop-opacity:1" />
<stop offset="100%" style="stop-color:#FFFFFF;stop-opacity:1" />
</linearGradient>
</defs>
<style type="text/css">
.st0{fill:#030104;}
/* Gradient fill with contrasting stroke */
.fill-gradient{fill:url(#iconGradient);}
.stroke-contrast{stroke:#2a2a2a;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;}
</style>
<g>
<g>
<path class="st0" d="M43,40c-0.9-7.2-2.3-13.9-5.7-20.2c-1-1.9-2.6-3.5-3.8-5.3c-0.2-0.3-0.5-0.7-0.7-1.1c0.7-0.6,1.4-1.3,2.1-2
<path class="fill-gradient stroke-contrast" d="M43,40c-0.9-7.2-2.3-13.9-5.7-20.2c-1-1.9-2.6-3.5-3.8-5.3c-0.2-0.3-0.5-0.7-0.7-1.1c0.7-0.6,1.4-1.3,2.1-2
c0.8-0.8,1.5-1.5,2.1-2.3c0.7-0.8,1.2-1.5,1.7-2.1c1-1.3,1.5-2.3,1.5-2.3s-1,0.5-2.3,1.5c-0.7,0.5-1.4,1.1-2.1,1.7
c-0.8,0.7-1.5,1.4-2.3,2.1c-0.4,0.4-0.9,0.9-1.3,1.4c-0.7-3.4-3.5-6.2-7-6.6c-3.7-0.5-7.6,1.7-8.5,5.4c-0.1,0.5-0.3,1-0.4,1.4
c-0.5-0.5-1-1-1.5-1.5C14,9.3,13.2,8.6,12.5,8c-0.8-0.7-1.5-1.2-2.1-1.7C9,5.3,8,4.8,8,4.8s0.5,1,1.5,2.3c0.5,0.7,1.1,1.4,1.7,2.1
@@ -17,10 +24,10 @@
c0.5,0.1,1,0,1.4-0.2c0.4-0.2,0.8-0.6,1.1-1.1c0.5-0.7,0.7-0.3,1.3,0.6c0.5,0.9,1.6,1.2,3,1.4c2.9,0.4,2.8,0.6,2.5,3.5
c0,0.5,0,1-0.1,1.4c-0.5,2.3,0.1,5.1-1.8,6.9c-0.8,0.7-1.6,1.1-1.5,1.2c0.1,0.1,1.1,0,2.1-0.1c2.4-0.3,4.9-0.7,7.4-1
C42.3,42.1,43.1,41.1,43,40z M21.3,8.2C20.2,9,19.2,9.3,19,9s0.4-1.2,1.5-1.9c1-0.7,2.1-1.1,2.3-0.7C23,6.6,22.3,7.5,21.3,8.2z"/>
<path class="st0" d="M32,35.9c0-0.2-0.9-0.2-1.9-0.1c-1,0.1-1.8,0.4-1.8,0.7c0,0.2,0.9,0.3,1.9,0.1C31.3,36.4,32,36.1,32,35.9z"/>
<path class="st0" d="M18.2,35.8c-0.9-0.2-1.7-0.1-1.8,0.1c0,0.2,0.7,0.5,1.6,0.7c0.9,0.2,1.7,0.1,1.8-0.1
<path class="fill-gradient stroke-contrast" d="M32,35.9c0-0.2-0.9-0.2-1.9-0.1c-1,0.1-1.8,0.4-1.8,0.7c0,0.2,0.9,0.3,1.9,0.1C31.3,36.4,32,36.1,32,35.9z"/>
<path class="fill-gradient stroke-contrast" d="M18.2,35.8c-0.9-0.2-1.7-0.1-1.8,0.1c0,0.2,0.7,0.5,1.6,0.7c0.9,0.2,1.7,0.1,1.8-0.1
C19.9,36.3,19.2,36,18.2,35.8z"/>
<path class="st0" d="M25.3,40.3c-0.1,0-0.3,0-0.5-0.1c-0.1,0-0.2-0.1-0.3-0.1c-0.1,0-0.2,0-0.2-0.1c-0.1,0-0.3-0.1-0.4,0
<path class="fill-gradient stroke-contrast" d="M25.3,40.3c-0.1,0-0.3,0-0.5-0.1c-0.1,0-0.2-0.1-0.3-0.1c-0.1,0-0.2,0-0.2-0.1c-0.1,0-0.3-0.1-0.4,0
c-0.1,0-0.3,0-0.4,0c-0.1,0-0.2,0.1-0.2,0.1c-0.1,0-0.2,0.1-0.2,0.1c-0.2,0.1-0.4,0-0.5,0.1c-0.1,0-0.2,0.1-0.2,0.1s0,0.1,0,0.3
c0,0.2,0,0.3,0.1,0.6c0.1,0.1,0.1,0.2,0.3,0.3c0.1,0.1,0.2,0.2,0.4,0.3c0.1,0.1,0.3,0.2,0.4,0.2c0.2,0,0.3,0.1,0.5,0.1
c0.2,0,0.3,0,0.5-0.1c0.2,0,0.3-0.1,0.4-0.2c0.3-0.1,0.5-0.4,0.6-0.6c0.1-0.2,0.2-0.4,0.2-0.6c0-0.2,0-0.2,0-0.2

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="48.2px" height="48.2px" viewBox="0 0 48.2 48.2" style="enable-background:new 0 0 48.2 48.2;" xml:space="preserve">
width="48.2px" height="48.2px" viewBox="-1 -1 50.2 50.2" style="enable-background:new -1 -1 50.2 50.2;" xml:space="preserve">
<style type="text/css">
.st0{fill:#1D1C1A;}
.st0{fill:#030104;}
</style>
<g>
<path class="st0" d="M48.7,4.8c0,0-0.1,0-0.2,0.1c-0.9,0.6-4.4,2-9.3,3.1c-4.6,1-10.8,1.8-16.7,2.3c-6.2,0.6-14.9,0.8-21.3-0.5

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -262,7 +262,7 @@
}
},
{
"id": "Journal of observations",
"id": "Journal of Observations",
"name": "Journal d'observations",
"description": "",
"source_reference": {
@@ -972,6 +972,198 @@
"source_reference": {
"page": ""
}
},
{
"id": "Glass Ornament (Dragonfly)",
"name": "Le Verre de Libellule",
"description": "",
"source_reference": {
"page": ""
}
},
{
"id": "Arrows: Armor-Piercing",
"name": "Flèches : Perce-armure",
"description": "",
"source_reference": {
"page": "236"
}
},
{
"id": "Arrows: Flesh-Cutter",
"name": "Flèches : Fouilleuse 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": {
"HideDisabledSources": {
"Title": "[Compendium] Hide sources filter without reference",
"Hint": "Hide empty source with no elements in source filter."
"Title": "[Compendium] Hide unavailable sources",
"Hint": "Hide sources that have no available content from the source filter dropdown."
},
"HideEmptySourcesFromPlayers": {
"Title": "[Compendium] Hide elements with empty reference",
@@ -136,6 +136,7 @@
"player_filter_label": "Player filter",
"player_filter_tooltip": "Apply player filter",
"already_in_filter": "Already in filter",
"no_results": "Not Found",
"sources_categories": {
"rules": "Rules",
"adventures": "Adventures",
@@ -492,7 +493,10 @@
"rarity_modifier": "Rarity modifier",
"item_pattern": "Item Patterns",
"signature_scroll": "Signature Scrolls",
"school_curriculum_journal": "Drop curriculum's journal in sheet to link it"
"school_curriculum_journal": "Drop curriculum's journal in sheet to link it",
"warning": {
"total_less_then_spent": "Total Experience is less then Used Experience."
}
},
"character_types": {
"character": "Player Character",
@@ -807,14 +811,15 @@
"filter": {
"rank": "Rank",
"rarity": "Rarity",
"ring": "Ring"
"ring": "Ring",
"clear": "Clear Filter"
}
},
"source_reference": {
"core_rulebook": "Core Rulebook",
"emerald_empire": "Emerald Empire",
"shadowlands": "Shadowlands",
"court_of_stones": "Court of Stones",
"court_of_stones": "Courts of Stone",
"path_of_waves": "Path of Waves",
"celestial_realms": "Celestial Realms",
"fields_of_victory": "Fields of Victory",

View File

@@ -136,6 +136,7 @@
"player_filter_label": "Filtro de jugador",
"player_filter_tooltip": "Aplicar filtro de jugador",
"already_in_filter": "Ya en el filtro",
"no_results": "Not Found",
"sources_categories": {
"rules": "Reglas",
"adventures": "Aventuras",
@@ -492,7 +493,10 @@
"rarity_modifier": "Modificador de rareza",
"item_pattern": "Patrones de objetos",
"signature_scroll": "Pergaminos espaciales",
"school_curriculum_journal": "Arrastra el diario del programa en la hoja para vincularlo"
"school_curriculum_journal": "Arrastra el diario del programa en la hoja para vincularlo",
"warning": {
"total_less_then_spent": "La experiencia total es menor que la experiencia utilizada."
}
},
"character_types": {
"character": "Personaje jugador",
@@ -807,7 +811,8 @@
"filter": {
"rank": "Rango",
"rarity": "Rareza",
"ring": "Anillo"
"ring": "Anillo",
"clear": "Clear Filter"
}
},
"source_reference": {

View File

@@ -136,6 +136,7 @@
"player_filter_label": "Filtre joueur",
"player_filter_tooltip": "Applique le filtre des joueurs",
"already_in_filter": "Filtre déjà appliqué",
"no_results": "Aucun résultat",
"sources_categories": {
"rules": "Règles",
"adventures": "Aventures",
@@ -492,7 +493,10 @@
"rarity_modifier": "Modificateur de rareté",
"item_pattern": "Procédés de fabrication",
"signature_scroll": "Rouleaux de marque",
"school_curriculum_journal": "Déposer un journal de Cursus dans la feuille pour le lier"
"school_curriculum_journal": "Déposer un journal de Cursus dans la feuille pour le lier",
"warning": {
"total_less_then_spent": "L'expérience totale est inférieure à l'expérience utilisée."
}
},
"character_types": {
"character": "Personnage Joueur",
@@ -807,7 +811,8 @@
"filter": {
"rank": "Rang",
"rarity": "Rareté",
"ring": "Anneau"
"ring": "Anneau",
"clear": "Suppr. les Filtres"
}
},
"source_reference": {

View File

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

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":"L5RCoreIte000031","name":"Scroll satchel","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"3","zeni":"1 bu","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"60"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000032","name":"Traveling pack","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"1","zeni":"10 koku","properties":[{"id":"L5RCorePro000012","name":"Mundane"}],"description":"","source_reference":{"source":"core_rulebook","page":"245"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000033","name":"Journal of observations","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"3","zeni":"1 bu","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"67"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000033","name":"Journal of Observations","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"3","zeni":"1 bu","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"67"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000034","name":"Omamori (Boon of Jurojin)","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"5 bu","properties":[],"description":"","source_reference":{"source":"celestial_realms","page":"97"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000035","name":"Omamori (Boon of Kisshoten)","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"5 bu","properties":[],"description":"","source_reference":{"source":"celestial_realms","page":"97"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000036","name":"Omamori (Boon of Hotei)","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"5 bu","properties":[],"description":"","source_reference":{"source":"celestial_realms","page":"97"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
@@ -119,3 +119,27 @@
{"_id":"L5RCoreIte000121","name":"Word of the Prophet","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"9","zeni":"15 Koku","properties":[],"description":"","source_reference":{"source":"children_of_the_five_winds","page":"103"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000122","name":"Talisman of the Sun [Blessed Treasure]","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"10","zeni":"0","properties":[],"description":"","source_reference":{"source":"children_of_the_five_winds","page":"104"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000123","name":"Fox Pipe [Blessed Treasure]","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"10","zeni":"0","properties":[],"description":"","source_reference":{"source":"children_of_the_five_winds","page":"105"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000124","name":"Glass Ornament (Dragonfly)","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"6","zeni":"1 koku","properties":[],"description":"","source_reference":{"source":"writ_of_the_wild","page":"83"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000125","name":"Arrows: Armor-Piercing","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"0","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"236"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000126","name":"Arrows: Flesh-Cutter","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"0","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"236"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000127","name":"Arrows: Humming-Bulb","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"0","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"236"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000128","name":"Journal","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"1","zeni":"0","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"66"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000129","name":"Smithing hammer","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"1","zeni":"0","properties":[],"description":"","source_reference":{"source":"shadowlands","page":"89"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000130","name":"Sumai Garb","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"1","zeni":"0","properties":[],"description":"","source_reference":{"source":"fields_of_victory","page":"89"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000131","name":"Drafting Paper","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"1","zeni":"0","properties":[],"description":"","source_reference":{"source":"celestial_realms","page":"84"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000132","name":"Fine set of Chisels","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"1","zeni":"0","properties":[],"description":"","source_reference":{"source":"celestial_realms","page":"84"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000133","name":"Omamori (Boon of Fukurokujin)","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"5 bu","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"243"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000134","name":"Omamori (Boon of Bishamon)","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"5 bu","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"243"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000135","name":"Omamori (Boon of Benten)","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"5 bu","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"243"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000136","name":"Poison - Noxious Poison (One Vial)","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"5","zeni":"30 zeni","properties":[{"id":"L5RCorePro000009","name":"Forbidden"}],"description":"","source_reference":{"source":"core_rulebook","page":"244"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000137","name":"Poison - Fire Biter (One Vial)","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"5","zeni":"30 zeni","properties":[{"id":"L5RCorePro000009","name":"Forbidden"}],"description":"","source_reference":{"source":"core_rulebook","page":"244"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000138","name":"Poison - Night Milk (One Vial)","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"5","zeni":"30 zeni","properties":[{"id":"L5RCorePro000009","name":"Forbidden"}],"description":"","source_reference":{"source":"core_rulebook","page":"244"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000139","name":"Blanket","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"0","zeni":"1 zeni","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"245"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000140","name":"Bowl","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"0","zeni":"1 zeni","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"245"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000141","name":"Flint and Tinder","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"0","zeni":"1 zeni","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"245"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000142","name":"Furoshiki","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"0","zeni":"1 zeni","properties":[],"description":"","source_reference":{"source":"core_rulebook","page":"245"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000143","name":"The Obsidian Journal","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"10","zeni":"0","properties":[{"id":"L5RCorePro000009","name":"Forbidden"},{"id":"L5RCorePro000008","name":"Unholy"}],"description":"","source_reference":{"source":"core_rulebook","page":"127"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000144","name":"Pouch of Insence","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"0","properties":[],"description":"","source_reference":{"source":"celestial_realms","page":"85"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000145","name":"Religious texts","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"0","properties":[],"description":"","source_reference":{"source":"celestial_realms","page":"86"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000146","name":"Small Sachel of Ingredients","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"10 bu","properties":[],"description":"","source_reference":{"source":"celestial_realms","page":"80"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}
{"_id":"L5RCoreIte000147","name":"Blessed Glass vial","permission":{"default":0},"type":"item","data":{"equipped":false,"quantity":1,"weight":"0","rarity":"2","zeni":"0","properties":[],"description":"","source_reference":{"source":"celestial_realms","page":"80"}},"sort":100001,"flags":{},"img":"systems/l5r5e/assets/icons/items/item.svg","effects":[]}

View File

@@ -624,6 +624,36 @@ export class BaseCharacterSheetL5r5e extends BaseSheetL5r5e {
});
break;
case "honor":
await this.actor.update({
system: {
social: {
honor: Math.max(0, this.actor.system.social.honor + mod),
},
},
});
break;
case "glory":
await this.actor.update({
system: {
social: {
glory: Math.max(0, this.actor.system.social.glory + mod),
},
},
});
break;
case "status":
await this.actor.update({
system: {
social: {
status: Math.max(0, this.actor.system.social.status + mod),
},
},
});
break;
default:
console.warn("L5R5E | BCS | Unsupported type", type);
break;

View File

@@ -56,6 +56,9 @@ export class CharacterSheetL5r5e extends BaseCharacterSheetL5r5e {
// Split Others advancements, and calculate xp spent and add it to total
this._prepareOthersAdvancement(sheetData);
// Update spent_xp to actor
this.actor.system.xp_spent = sheetData.data.system.xp_spent;
// Total
sheetData.data.system.xp_saved = Math.floor(
parseInt(sheetData.data.system.xp_total) - parseInt(sheetData.data.system.xp_spent)
@@ -114,6 +117,9 @@ export class CharacterSheetL5r5e extends BaseCharacterSheetL5r5e {
// Money +/-
html.find(".money-control").on("click", this._modifyMoney.bind(this));
// XP +/-
html.find(".xp-control").on("click", this._modifyXP.bind(this));
// Advancements Tab to current rank onload
// TODO class "Active" Bug on load, dunno why :/
this._tabs
@@ -149,6 +155,12 @@ export class CharacterSheetL5r5e extends BaseCharacterSheetL5r5e {
adv[rank].spent.total += xp_used_total;
adv[rank].spent.curriculum += xp_used;
});
// If we finished the rank but haven't added anything to the next rank we should show an empty tab
// note: adv is index from 1, not 0 because of rank starting at 1
if(adv.length -1 < sheetData.data.system.identity.school_rank) {
adv.push({list: [], rank: sheetData.data.system.identity.school_rank, spent: { total: 0, curriculum: 0}});
}
sheetData.data.advancementsListByRank = adv;
}
@@ -285,6 +297,35 @@ export class CharacterSheetL5r5e extends BaseCharacterSheetL5r5e {
this.render(false);
}
/**
* Add or Subtract XP (+/- buttons)
* @param {Event} event
* @private
*/
async _modifyXP(event) {
event.preventDefault();
event.stopPropagation();
const elmt = $(event.currentTarget);
let mod = elmt.data("value");
if (!mod) {
return;
}
const new_xp_total = Math.max(0, this.actor.system.xp_total + mod);
this.actor.update({
system: {
xp_total: new_xp_total,
},
});
if(this.actor.system.xp_spent > new_xp_total) {
ui.notifications.warn("l5r5e.advancements.warning.total_less_then_spent", { localize: true })
}
this.render(false);
}
/**
* Add +1 to actor school rank
* @param {Event} event

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

@@ -1,47 +1,110 @@
import { L5r5eHtmlMultiSelectElement } from "../misc/l5r5e-multiselect.js";
/**
* A subclass of [ArrayField]{@link ArrayField} which supports a set of contained elements.
* Elements in this set are treated as fungible and may be represented in any order or discarded if invalid.
* A Foundry `SetField` that renders as an {@link L5r5eHtmlMultiSelectElement} chip-input.
*
* Use this in a DataModel schema whenever a field stores an unordered collection of
* string values drawn from a fixed option list. On form submission the element returns a
* comma-separated string; `clean()` splits it back into an Array before Foundry processes
* it, and `initialize()` wraps the result in a `Set` for use in the model.
*
* @example
* // In a DataModel schema:
* skills: new L5r5eSetField({
* options: [
* { value: "athletics", label: "Athletics" },
* { value: "meditation", label: "Meditation", disabled: true, tooltip: "Requires rank 3" },
* ]
* })
*
* // Renders automatically via {{formGroup}} in a Handlebars template:
* // {{formGroup fields.skills name="skills" value=data.skills localize=true}}
*
* @param {object} options
* @param {{ value: string, label: string, disabled?: boolean, tooltip?: string }[]} options.options
* Flat list of selectable items. Passed directly to {@link L5r5eHtmlMultiSelectElement.create}.
* @param {object[]} [options.groups]
* Optional optgroup definitions, forwarded to the element factory unchanged.
* @param {boolean} [options.hideDisabledOptions=false]
* When true, disabled options are hidden from the dropdown instead of greyed out.
*/
export class L5r5eSetField extends foundry.data.fields.SetField {
/**
* 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
#savedOptions;
/**
* @param {object} options
* @param {object} context
*/
constructor(options = {}, context = {}) {
super(
new foundry.data.fields.StringField({
choices: options.options?.map((option) => option.value) ?? [],
}),
options,
context
);
constructor(options={}, context={}) {
super(new foundry.data.fields.StringField({
choices: options.options.map((option) => option.value)
}), options, context);
this.#savedOptions = options;
}
/** @override */
initialize(value, model, options={}) {
if ( !value ) return value;
return new Set(super.initialize(value, model, options));
this.#savedOptions = 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) {
if ( !value ) return value;
return Array.from(value).map(v => this.element.toObject(v));
if (!value) {
return value;
}
return Array.from(value).map((v) => this.element.toObject(v));
}
/* -------------------------------------------- */
/* Form Field Integration */
/* -------------------------------------------- */
/**
* @param {string|Array} value
* @param {object} options
* @return {Array}
* @override
*/
clean(value, options) {
// Settings forms submit comma-separated strings; split before normal cleaning.
if (typeof value === "string") {
value = value.length ? value.split(",").filter(Boolean) : [];
}
return super.clean(value, options);
}
/** @override */
/**
* @param {object} config
* @return {L5r5eHtmlMultiSelectElement}
* @override
*/
_toInput(config) {
const e = this.element;
return L5r5eHtmlMultiSelectElement.create({
name: config.name,
options: this.#savedOptions.options,
groups: this.#savedOptions.groups,
value: config.value,
localize: config.localize
});
return L5r5eHtmlMultiSelectElement.create({
name: config.name,
options: this.#savedOptions.options,
groups: this.#savedOptions.groups,
value: config.value,
localize: config.localize,
hideDisabledOptions: this.#savedOptions.hideDisabledOptions,
});
}
}
}

View File

@@ -513,6 +513,9 @@ export class DicePickerDialog extends FormApplication {
this._updateVoidPointUsage();
this.render(false);
});
// Open journal on effect name
html.find(".effect-name").on("click", this._openEffectJournal.bind(this));
}
/**
@@ -864,4 +867,34 @@ export class DicePickerDialog extends FormApplication {
return array;
}
/**
* Open the core linked journal effect if exist
* @param {Event} event
* @private
*/
async _openEffectJournal(event) {
event.preventDefault();
event.stopPropagation();
const effectId = $(event.currentTarget).data("effect-id");
if (!effectId) {
return;
}
const effect = this._actor?.effects?.get(effectId);
if (!effect?.system?.id && !effect?.system?.uuid) {
return;
}
const journal = await game.l5r5e.HelpersL5r5e.getObjectGameOrPack({
id: effect.system.id,
uuid: effect.system.uuid,
type: "JournalEntry",
});
if (journal) {
// Open on the "rules" section. If non exists then it will open the first page
journal.sheet.render(true, {pageIndex: 2});
}
}
}

View File

@@ -268,6 +268,9 @@ export class RollnKeepDialog extends FormApplication {
], { jQuery: false });
}
// Open journal on effect name
html.find(".effect-name").on("click", this._openEffectJournal.bind(this));
// *** Everything below here is only needed if the sheet is editable ***
if (!this.isEditable) {
return;
@@ -802,4 +805,34 @@ export class RollnKeepDialog extends FormApplication {
// Re-enable the button
button.attr("disabled", false);
}
/**
* Open the core linked journal effect if exist
* @param {Event} event
* @private
*/
async _openEffectJournal(event) {
event.preventDefault();
event.stopPropagation();
const effectId = $(event.currentTarget).data("effect-id");
if (!effectId) {
return;
}
const effect = this.roll.l5r5e?.actor?.effects?.get(effectId);
if (!effect?.system?.id && !effect?.system?.uuid) {
return;
}
const journal = await game.l5r5e.HelpersL5r5e.getObjectGameOrPack({
id: effect.system.id,
uuid: effect.system.uuid,
type: "JournalEntry",
});
if (journal) {
// Open on the "rules" section. If non exists then it will open the first page
journal.sheet.render(true, {pageIndex: 2});
}
}
}

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 */

View File

@@ -5,10 +5,11 @@ export class GmToolbox extends HandlebarsApplicationMixin(ApplicationV2) {
/** @override ApplicationV2 */
static get DEFAULT_OPTIONS() { return {
id: "l5r5e-gm-toolbox",
classes: ["faded-ui"],
window: {
contentClasses: ["l5r5e", "gm-toolbox", "faded-ui"],
contentClasses: ["l5r5e", "gm-toolbox"],
title: "l5r5e.gm.toolbox.title",
minimizable: true,
minimizable: false,
},
position: {
width: "auto",
@@ -89,6 +90,19 @@ export class GmToolbox extends HandlebarsApplicationMixin(ApplicationV2) {
options.position.left = 220; //x - 630;
}
async _onRender(context, options) {
await super._onRender(context, options);
if (this.options.classes.includes("themed")) {
return;
}
this.element.classList.remove("theme-light", "theme-dark");
const {colorScheme} = game.settings.get("core", "uiConfig");
if (colorScheme.interface) {
this.element.classList.add("themed", `theme-${colorScheme.interface}`);
}
}
/**
* The GM Toolbox should not be removed when toggling the main menu with the esc key etc.
* @override ApplicationV2

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 {
/**
@@ -26,6 +26,8 @@ export default class HooksL5r5e {
) {
game.babele.setSystemTranslationsDir("babele"); // Since Babele v2.0.7
}
ItemCompendiumL5r5e.applyToPacks();
}
/**
@@ -245,236 +247,6 @@ export default class HooksL5r5e {
});
}
/**
* Compendium display (Add filters)
*/
static async renderCompendium(app, html, data) {
html = $(html); // basic patch for v13
if (app.collection.documentName === "Item") {
const content = await app.collection.getDocuments();
const sourcesInThisCompendium = new Set([]);
const filtersToShow = {
rank: false,
rarity: false,
source: false,
ring: false,
};
// Used to auto hide same values for a full compendium
const previousValue = {
rank: null,
rarity: null,
source: null,
ring: null,
};
// Cache
const header = html.find(".directory-header");
const entries = html.find(".directory-item");
// Add additional data to the entries to make it faster to lookup.
// Add Ring/rank/rarity information
for (const document of content) {
const entry = entries.filter(`[data-entry-id="${document.id}"]`);
// Hide filter if only one value of this type is found in the compendium
const autoDisplayFilter = (props, documentData = null) => {
documentData ??= document.system[props];
if (filtersToShow[props] || previousValue[props] === documentData) {
return;
}
filtersToShow[props] = previousValue[props] !== null && previousValue[props] !== documentData;
previousValue[props] = documentData;
};
if (document.system?.rank) {
autoDisplayFilter('rank');
entry.data("rank", document.system.rank);
}
if (document.system?.source_reference.source) {
autoDisplayFilter('source', document.system.source_reference.source);
sourcesInThisCompendium.add(document.system.source_reference.source);
entry.data("source", document.system.source_reference);
}
if (document.system?.ring) {
autoDisplayFilter('ring');
entry.data("ring", document.system.ring);
}
if (document.system?.rarity) {
autoDisplayFilter('rarity');
entry.data("rarity", document.system.rarity);
}
// Add ring/rank/rarity information on the item in the compendium view
if (document.system?.ring || document.system?.rarity || document.system?.rank) {
const ringRarityRank = await foundry.applications.handlebars.renderTemplate(`${CONFIG.l5r5e.paths.templates}compendium/ring-rarity-rank.html`, document.system);
entry.append(ringRarityRank);
}
}
// Setup filters
const officialContentSet = game.settings.get(CONFIG.l5r5e.namespace, "compendium-official-content-for-players");
const unofficialContentSet = game.settings.get(CONFIG.l5r5e.namespace, "compendium-unofficial-content-for-players");
const allCompendiumReferencesSet = game.settings.get(CONFIG.l5r5e.namespace, "all-compendium-references")
const hideEmptySourcesFromPlayers = game.settings.get(CONFIG.l5r5e.namespace, "compendium-hide-empty-sources-from-players");
const unavailableSourceForPlayersSet = new Set([...allCompendiumReferencesSet].filter((element) => {
if (CONFIG.l5r5e.sourceReference[element]) {
return officialContentSet.size > 0 ? !officialContentSet.has(element) : false;
}
return unofficialContentSet.size > 0 ? !unofficialContentSet.has(element) : false;
}));
// Create filter function
const applyCompendiumFilter = () => {
const userFilter = header.find("l5r5e-multi-select").val();
const rankFilter = header.find(".rank-filter .selected").data("rank");
const ringFilter = header.find(".ring-filter .selected").data("ring");
const rarityFilter = header.find(".rarity-filter .selected").data("rarity");
entries.each(function () {
const lineSource = $(this).data("source")?.source;
// We might have stuff in the compendium view that does not have a source (folders etc.) Ignore those.
if (lineSource === null || lineSource === undefined) {
return;
}
let shouldShow = true;
// Handle unavailable sources
if (unavailableSourceForPlayersSet.has(lineSource)) {
if (game.user.isGM) {
shouldShow &= true;
$(this)
.addClass("not-for-players")
.attr("data-tooltip", game.i18n.localize("l5r5e.compendium.not_for_players"));
} else {
shouldShow &= false;
}
}
// Handle empty sources
if (lineSource === "" && hideEmptySourcesFromPlayers) {
if (game.user.isGM) {
shouldShow &= true;
$(this)
.addClass("not-for-players")
.attr("data-tooltip", game.i18n.localize("l5r5e.compendium.not_for_players"));
} else {
shouldShow &= false;
}
}
// Apply filters
if (rankFilter) {
shouldShow &= $(this).data("rank") == rankFilter;
}
if (userFilter?.length) {
shouldShow &= userFilter.includes(lineSource);
}
if (ringFilter) {
shouldShow &= $(this).data("ring") == ringFilter;
}
if (rarityFilter >= 0) {
shouldShow &= $(this).data("rarity") == rarityFilter;
}
// Show or hide this entry based on the result
shouldShow ? $(this).show() : $(this).hide();
});
};
// Filter setup
const addFilter = async (filterType, templateFile, templateData) => {
if (!filtersToShow[filterType]) {
return;
}
const filterTemplate = await foundry.applications.handlebars.renderTemplate(
`${CONFIG.l5r5e.paths.templates}compendium/${templateFile}.html`,
templateData
);
header.append(filterTemplate);
header.find(`.${filterType}-filter`).children().each(function () {
$(this).on("click", (event) => {
const selected = $(event.target).hasClass("selected");
header.find(`.${filterType}-filter`).children().removeClass("selected");
$(event.target).toggleClass("selected", !selected);
applyCompendiumFilter();
});
});
};
// Add Rank, Rarity, Ring Filters
await Promise.all([
addFilter('rank' , 'rank-filter', { type: "rank", number: [1, 2, 3, 4, 5] }),
addFilter('rarity', 'rank-filter', { type: "rarity", number: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }),
addFilter('ring' , 'ring-filter', {}),
]);
if (filtersToShow.source) {
// Build the source select
const selectableSourcesArray = [...allCompendiumReferencesSet].map((reference) => ({
value: reference,
label: CONFIG.l5r5e.sourceReference[reference]?.label ?? reference,
translate: true,
group: CONFIG.l5r5e.sourceReference[reference]?.type.split(",")[0] ?? "l5r5e.multiselect.sources_categories.others",
disabled: !sourcesInThisCompendium.has(reference) || (!game.user.isGM && unavailableSourceForPlayersSet.has(reference))
}));
const filterSourcesBox = L5r5eHtmlMultiSelectElement.create({
name: "filter-sources",
options: selectableSourcesArray,
localize: true,
});
header.append(filterSourcesBox.outerHTML);
$("l5r5e-multi-select").on("change", applyCompendiumFilter);
// If gm add an extra button to easily filter the content to see the same stuff as a player
if (game.user.isGM && unavailableSourceForPlayersSet.size > 0) {
const buttonHTML = `<button type="button" class="gm" data-tooltip="${game.i18n.localize('l5r5e.multiselect.player_filter_tooltip')}">`
+ game.i18n.localize('l5r5e.multiselect.player_filter_label')
+ '</button>'
const filterPlayerViewArray = [...allCompendiumReferencesSet]
.filter((item) => !unavailableSourceForPlayersSet.has(item))
.filter((item) => sourcesInThisCompendium.has(item));
$(buttonHTML).appendTo($(header).find("l5r5e-multi-select")).click(function() {
header.find("l5r5e-multi-select")[0].value = filterPlayerViewArray;
});
}
}
// TODO: This delay is a workaround and should be addressed in another way.
// This is ugly but if we hide the content too early then it won't be hidden for some reason.
// Current guess is that the foundry search filter is doing something.
// Adding a delay here so that we hide the content. This will fail on slow computers/network...
setTimeout(() => {
applyCompendiumFilter();
}, 250);
return false;
}
}
static updateCompendium(pack, documents, options, userId) {
documents.forEach((document) => {
const inc_reference = document?.system?.source_reference?.source?.trim();
if (!!inc_reference) {
const references = game.settings.get(CONFIG.l5r5e.namespace, "all-compendium-references");
if (!references.includes(inc_reference)) {
references.push(inc_reference);
game.settings.set(CONFIG.l5r5e.namespace, "all-compendium-references", references);
}
}
});
}
/**
* DiceSoNice - Add L5R DicePresets
*/

View File

@@ -38,6 +38,8 @@ import { ArmyFortificationSheetL5r5e } from "./items/army-fortification-sheet.js
// JournalEntry
import { JournalL5r5e } from "./journal.js";
import { BaseJournalSheetL5r5e } from "./journals/base-journal-sheet.js";
// Compendium
import { CompendiumDirectoryL5r5e } from "./compendium/l5r5e-compendium-directory.js";
// Specific
import { MigrationL5r5e } from "./migration.js";
import { GmToolbox } from "./gm/gm-toolbox.js";
@@ -45,8 +47,10 @@ import { GmMonitor } from "./gm/gm-monitor.js";
import { Storage } from "./storage.js";
// Misc
import { L5r5eHtmlMultiSelectElement } from "./misc/l5r5e-multiselect.js";
import { L5R5eHtmlComboBoxElement } from "./misc/l5r5e-combo-box.js";
window.customElements.define(L5r5eHtmlMultiSelectElement.tagName, L5r5eHtmlMultiSelectElement);
window.customElements.define(L5R5eHtmlComboBoxElement.tagName, L5R5eHtmlComboBoxElement);
/* ------------------------------------ */
/* Initialize system */
@@ -66,6 +70,23 @@ Hooks.once("init", async () => {
// Global access to L5R Config
CONFIG.l5r5e = L5R5E;
// Setting up sidebar icons
CONFIG.ChatMessage.sidebarIcon = "l5r5e chatIcon";
CONFIG.Combat.sidebarIcon = "l5r5e combatIcon";
CONFIG.Scene.sidebarIcon = "l5r5e sceneIcon";
CONFIG.Actor.sidebarIcon = "l5r5e actorIcon";
CONFIG.Item.sidebarIcon = "l5r5e itemIcon";
CONFIG.JournalEntry.sidebarIcon = "l5r5e journalIcon";
CONFIG.RollTable.sidebarIcon = "l5r5e rolltableIcon";
CONFIG.Playlist.sidebarIcon = "l5r5e playlistIcon";
// Note: We don't have any custom icons here so just append l5r5e and type
CONFIG.Cards.sidebarIcon += " l5r5e cardsIcon";
CONFIG.Macro.sidebarIcon += " l5r5e macroIcon";
// The compendium and the settings menu is registered a little different.
foundry.applications.sidebar.Sidebar.TABS.compendium.icon = "l5r5e compendiumIcon";
foundry.applications.sidebar.Sidebar.TABS.settings.icon = "l5r5e settingsIcon";
// Assign custom classes and constants here
CONFIG.Combat.documentClass = CombatL5r5e;
CONFIG.Actor.documentClass = ActorL5r5e;
@@ -76,6 +97,8 @@ Hooks.once("init", async () => {
CONFIG.Token.rulerClass = TokenRulerL5r5e;
CONFIG.Canvas.rulerClass = RulerL5r5e;
CONFIG.ui.compendium = CompendiumDirectoryL5r5e;
// Define custom Roll class
CONFIG.Dice.rolls.unshift(RollL5r5e);
@@ -265,6 +288,4 @@ Hooks.on("renderSidebarTab", (app, html, data) => HooksL5r5e.renderSidebarTab(ap
Hooks.on("activateSettings", async (app)=> HooksL5r5e.activateSettings(app));
Hooks.on("renderChatMessageHTML", (message, html, data) => HooksL5r5e.renderChatMessage(message, html, data));
Hooks.on("renderCombatTracker", (app, html, data) => HooksL5r5e.renderCombatTracker(app, html, data));
Hooks.on("renderCompendium", async (app, html, data) => HooksL5r5e.renderCompendium(app, html, data));
Hooks.on("diceSoNiceRollStart", (messageId, context) => HooksL5r5e.diceSoNiceRollStart(messageId, context));
Hooks.on("updateCompendium", (pack, documents, options, userId) => HooksL5r5e.updateCompendium(pack, documents, options, userId));

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;
/**
* Provide a multi-select workflow using a select element as the input mechanism.
* It is a expanded copy of the HTMLMultiselect with support for disabling options
* and a clear all button. Also have support for hover-over information using titlea
* A custom `<l5r5e-multi-select>` form element providing Select2-style chip multi-selection.
*
* @example Multi-Select HTML Markup
* ```html
* <l5r5e-multi-select name="select-many-things">
* <optgroup label="Basic Options">
* <option value="foo">Foo</option>
* <option value="bar">Bar</option>
* <option value="baz">Baz</option>
* </optgroup>
* <optgroup label="Advanced Options">
* <option value="fizz">Fizz</option>
* <option value="buzz">Buzz</option>
* </optgroup>
* Stores **multiple string values** from a fixed option list, shown as removable chips inside
* the input box. A live-search input filters the dropdown as the user types. Use this when a
* field holds an unordered collection of values (e.g. a set of skills, tags, or abilities).
* For storing a single value — predefined or free-text — use {@link L5R5eHtmlComboBoxElement} instead.
*
* The element's `value` getter returns a comma-separated string (e.g. `"fire,water"`),
* which is what `FormData` will read on submission. `_getValue()` returns a plain Array,
* which is what `FormDataExtended` will use.
*
* Pre-selection on render is handled via the `value` attribute on the element — NOT via
* `{{selectOptions selected=...}}`, which cannot handle comma-separated strings. Use
* `{{selectOptions}}` without `selected` purely to render the available options, and let
* the `value` attribute drive pre-selection. Since `getAttribute()` always returns a string,
* passing a `Set` or `Array` via Handlebars will not work correctly — always pass a
* comma-separated string to `value=`.
*
* Prefer {@link L5r5eSetField} + `{{formGroup}}` when wiring this into a DataModel — the
* field handles the full round-trip automatically.
*
* @example
* ```hbs
* {{!-- Use value= (comma-separated string) for pre-selection, not selectOptions selected= --}}
* <l5r5e-multi-select name="elements" value="{{data.elements}}">
* {{selectOptions choices localize=true}}
* </l5r5e-multi-select>
* ```
*
* @example
* // Static factory — use only when building outside of Foundry's field/template system:
* const el = L5r5eHtmlMultiSelectElement.create({
* name: "elements",
* options: [{ value: "fire", label: "Fire" }, { value: "water", label: "Water" }],
* value: "fire,water", // comma-separated pre-selection
* });
* form.appendChild(el);
*
* // Reading the value back:
* el.value; // "fire,water" — comma-separated string, compatible with FormData
* el._getValue(); // ["fire","water"] — array, compatible with FormDataExtended
*/
export class L5r5eHtmlMultiSelectElement extends AbstractMultiSelectElement {
constructor() {
super();
this.#setup();
}
export class L5r5eHtmlMultiSelectElement extends DropdownMixin(
AbstractMultiSelectElement,
{ multiSelect: true, debounceMs: 150 }
) {
/** @override */
static tagName = "l5r5e-multi-select";
/**
* A select element used to choose options.
* @type {HTMLSelectElement}
*/
#select;
/** @type {HTMLDivElement} — outer box containing chips, input, clear button */
#selectionBox;
/** @type {HTMLDivElement} — chips are injected here */
#chipList;
/** @type {HTMLSpanElement} — auto-sizing wrapper around the search input */
#inputSizer;
/** @type {HTMLButtonElement} — trailing clear-all button */
#clearButton;
/** @type {Set<string>} */
#disabledValues = new Set();
/** @type {Map<string, string>} */
#tooltips = new Map();
/**
* A display element which lists the chosen options.
* @type {HTMLDivElement}
* Returns a comma-separated string
* FormData reads this via field.value.
* @override
*/
#tags;
get value() {
return Array.from(this._value).join(",");
}
/** @override */
set value(val) {
this._value.clear();
const values = Array.isArray(val) ? val : String(val).split(",").filter(Boolean);
for (const v of values) {
this._value.add(v);
}
this._internals.setFormValue(this.value);
this._refresh();
}
/**
* A button element which clear all the options.
* @type {HTMLButtonElement}
* Return an array so FormDataExtended.object[name] matches Foundry's own
* HTMLMultiSelectElement — both field.value (string) and .object (array) are correct.
* @override
* @protected
*/
#clearAll;
_getValue() {
return Array.from(this._value);
}
/**
* A Set containing the values that should always be disabled.
* @type {Set}
* Accept either an array or comma-separated string when Foundry calls _setValue().
* @override
* @protected
*/
#disabledValues;
/* -------------------------------------------- */
// We will call initialize twice (one in the parent constructor) then one in #setup
// required since when we want to build the elements we should to an initialize first
// and we cannot override _initialize since we don't have access to #disabledValues there
#setup() {
super._initialize();
this.#disabledValues = new Set();
for (const option of this.querySelectorAll("option")) {
if (option.value === "") {
option.label = game.i18n.localize("l5r5e.multiselect.empty_tag");
this._choices[option.value] = game.i18n.localize("l5r5e.multiselect.empty_tag");
}
if (option.disabled) {
this.#disabledValues.add(option.value);
}
_setValue(val) {
const values = Array.isArray(val) ? val : String(val).split(",").filter(Boolean);
if (values.some(v => v && !(v in this._choices))) {
throw new Error("The values assigned to a multi-select element must all be valid options.");
}
this._value.clear();
for (const v of values) {
this._value.add(v);
}
}
/** @override */
_initialize() {
super._initialize(); // fills this._choices, this._value, this._options
for (const option of this.querySelectorAll("option")) {
if (option.disabled)
this.#disabledValues.add(option.value);
if (option.title)
this.#tooltips.set(option.value, option.title);
}
if (this.hasAttribute("value")) {
this._setValue(this.getAttribute("value"));
}
}
/* -------------------------------------------- */
/* Element Lifecycle */
/* -------------------------------------------- */
/** @override */
_buildElements() {
this.#setup();
// Create select element
this.#select = this._primaryInput = document.createElement("select");
this.#select.insertAdjacentHTML("afterbegin", `<option id="l5r5e-multiselect-placeholder" value="" disabled selected hidden>${game.i18n.localize("l5r5e.multiselect.placeholder")}</option>`);
this.#select.append(...this._options);
this.#select.disabled = !this.editable;
// Create a div element for display
this.#tags = document.createElement("div");
this.#tags.className = "tags input-element-tags";
// Create a clear all button
this.#clearAll = document.createElement("button");
this.#clearAll.textContent = "X";
return [this.#select, this.#clearAll, this.#tags];
}
/* -------------------------------------------- */
/** @override */
_refresh() {
// Update the displayed tags
const tags = Array.from(this._value).map(id => {
return foundry.applications.elements.HTMLStringTagsElement.renderTag(id, this._choices[id], this.editable);
// Ask mixin to build <input> + <ul>, then re-home them into our structure.
const mixinWrapper = this._buildDropdownElements({
placeholder: this.getAttribute("placeholder")
?? game.i18n.localize("l5r5e.multiselect.placeholder"),
});
this.#tags.replaceChildren(...tags);
const searchInput = mixinWrapper.querySelector("input.input");
const dropdownList = mixinWrapper.querySelector("ul.dropdown");
// Figure out if we are overflowing the tag div.
if($(this.#tags).css("max-height")) {
const numericMaxHeight = parseInt($(this.#tags).css("max-height"), 10);
if(numericMaxHeight) {
if($(this.#tags).prop("scrollHeight") > numericMaxHeight) {
this.#tags.classList.add("overflowing");
}
else {
this.#tags.classList.remove("overflowing");
}
}
}
// Selection box
this.#selectionBox = document.createElement("div");
this.#selectionBox.classList.add("selection-box");
// Disable selected options
const hideDisabled = game.settings.get(CONFIG.l5r5e.namespace, "compendium-hide-disabled-sources");
for (const option of this.#select) {
if (this._value.has(option.value)) {
option.disabled = true;
option.title = game.i18n.localize("l5r5e.multiselect.already_in_filter");
continue;
}
if (this.#disabledValues.has(option.value)) {
option.disabled = true;
option.hidden = hideDisabled;
continue;
}
option.disabled = false;
option.removeAttribute("title");
}
// Chip list
this.#chipList = document.createElement("div");
this.#chipList.classList.add("chip-list");
// Auto-sizing sizer — CSS grid trick: ::after mirrors data-value, input shares the cell
this.#inputSizer = document.createElement("span");
this.#inputSizer.classList.add("input-sizer");
this.#inputSizer.dataset.value = "";
this.#inputSizer.append(searchInput);
// Clear-all button
this.#clearButton = document.createElement("button");
this.#clearButton.type = "button";
this.#clearButton.classList.add("clear-btn");
this.#clearButton.setAttribute("aria-label",
game.i18n.localize("l5r5e.multiselect.clear_all") ?? "Clear all");
this.#clearButton.textContent = "×";
this.#clearButton.hidden = true;
this.#selectionBox.append(this.#chipList, this.#inputSizer, this.#clearButton);
// Container: selection box + dropdown must share the same positioned ancestor.
const container = document.createElement("div");
container.classList.add("multi-select-container");
container.append(this.#selectionBox, dropdownList);
this._primaryInput = searchInput;
return [container];
}
/* -------------------------------------------- */
/** @override */
_activateListeners() {
this.#select.addEventListener("change", this.#onChangeSelect.bind(this));
this.#clearAll.addEventListener("click", this.#onClickClearAll.bind(this));
this.#tags.addEventListener("click", this.#onClickTag.bind(this));
this._activateDropdownListeners();
const signal = this.abortSignal;
this.#tags.addEventListener("mouseleave", this.#onMouseLeave.bind(this));
this.#selectionBox.addEventListener("mousedown", this.#onBoxMouseDown.bind(this), { signal });
this.#chipList.addEventListener("click", this.#onChipClick.bind(this), { signal });
this.#clearButton.addEventListener("click", this.#onClearAll.bind(this), { signal });
// stop the clear button from opening the selection box when pressing it
this.#clearButton.addEventListener("mousedown", (event) => {event.preventDefault(); event.stopPropagation();}, {signal});
this._dropdownInput.addEventListener("input", () => this.#updateInputSizer(), { signal });
}
#onMouseLeave(event) {
// Figure out if we are overflowing the tag div.
if($(this.#tags).css("max-height")) {
const numericMaxHeight = parseInt($(this.#tags).css("max-height"), 10);
if($(this.#tags).prop("scrollHeight") > numericMaxHeight) {
this.#tags.classList.add("overflowing");
}
else {
this.#tags.classList.remove("overflowing");
}
}
}
/* -------------------------------------------- */
/**
* Handle changes to the Select input, marking the selected option as a chosen value.
* @param {Event} event The change event on the select element
*/
#onChangeSelect(event) {
event.preventDefault();
event.stopImmediatePropagation();
const select = event.currentTarget;
if (select.valueIndex === 0)
return; // Ignore placeholder
this.select(select.value);
select.value = "";
}
/* -------------------------------------------- */
/**
* Handle click events on a tagged value, removing it from the chosen set.
* @param {PointerEvent} event The originating click event on a chosen tag
*/
#onClickTag(event) {
event.preventDefault();
if (!event.target.classList.contains("remove"))
return;
if (!this.editable)
return;
const tag = event.target.closest(".tag");
this.unselect(tag.dataset.key);
}
/* -------------------------------------------- */
/**
* Handle clickling the clear all button
* @param {Event} event The originating click event on the clear all button
*/
#onClickClearAll(event) {
event.preventDefault();
var _this = this;
$(this.#tags).children().each(function () {
_this.unselect($(this).data("key"));
})
}
/* -------------------------------------------- */
/** @override */
_toggleDisabled(disabled) {
this.#select.toggleAttribute("disabled", disabled);
this._toggleDropdownDisabled(disabled);
if (this.#selectionBox) {
this.#selectionBox.classList.toggle("disabled", disabled);
}
if (this.#chipList) {
this._refresh(); // re-render chips so × appears/disappears
}
}
/** @override */
_refresh() {
const values = Array.from(this._value);
this._internals.setFormValue(values.length ? values.join(",") : "");
this.#renderChips(values);
// Clear button: only visible when editable and something is selected.
if (this.#clearButton) {
this.#clearButton.hidden = (!this.editable || values.length === 0);
}
this.#updateInputSizer();
this._dropdownRefresh();
}
/** @param {string[]} values */
#renderChips(values) {
if (!this.#chipList){
return
}
this.#chipList.replaceChildren(...values.map(id => this.#buildChip(id)));
}
/** @param {string} id */
#buildChip(id) {
const chip = document.createElement("span");
chip.classList.add("chip");
chip.dataset.key = id;
const label = document.createElement("span");
label.classList.add("chip-label");
label.textContent = this._choices[id] ?? id;
chip.append(label);
// Only add × when the element is editable (not disabled, not readonly).
if (this.editable) {
const remove = document.createElement("span");
remove.classList.add("chip-remove");
remove.setAttribute("aria-label", `Remove ${this._choices[id] ?? id}`);
remove.setAttribute("aria-hidden", "true");
remove.textContent = "×";
chip.append(remove);
}
return chip;
}
/** Mirror typed text into the sizer span so CSS sizes the input correctly. */
#updateInputSizer() {
if (!this.#inputSizer || !this._dropdownInput)
return;
const input = this._dropdownInput;
const text = input.value || input.placeholder || "";
this.#inputSizer.dataset.value = text;
}
/** @override */
_getDropdownOptions() {
const makeOption = (option, group = null) => ({
value: option.value,
label: this._choices[option.value] ?? option.innerText,
group,
disabled: this.#disabledValues.has(option.value),
tooltip: this.#tooltips.get(option.value) ?? "",
});
return this._options.flatMap(child => {
if (child instanceof HTMLOptGroupElement) {
return [...child.querySelectorAll("option")]
.filter(option => option.value)
.map(option => makeOption(option, child.label));
}
if (child instanceof HTMLOptionElement && child.value) {
return makeOption(child);
}
return [];
});
}
/** @override */
_isOptionSelected(value) {
return this._value.has(value);
}
/** @override */
_onDropdownPick(option) {
const inValue = this._value.has(option.value);
const inChoices = option.value in this._choices;
if(!(inValue || inChoices))
return;
if (inValue) {
this._value.delete(option.value);
}
else if(inChoices) {
this._value.add(option.value);
}
this._internals.setFormValue(this.value);
this._refresh();
this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
}
#onBoxMouseDown(event) {
// Fully block interaction when not editable.
if (!this.editable) {
event.preventDefault();
return;
}
if (event.target.classList.contains("chip-remove"))
return;
if (event.target === this._dropdownInput)
return;
event.preventDefault();
this._dropdownInput?.focus();
}
#onChipClick(event) {
if (!event.target.classList.contains("chip-remove") || !this.editable)
return;
const chip = event.target.closest(".chip");
if (!chip)
return;
this._value.delete(chip.dataset.key);
this._internals.setFormValue(this.value);
this._refresh();
this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
this._dropdownInput?.focus();
}
#onClearAll(event) {
event.preventDefault();
if (!this.editable)
return;
this._value.clear();
this._internals.setFormValue("");
this._refresh();
this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
}
/* -------------------------------------------- */
/* Static Factory */
/* -------------------------------------------- */
/**
* Create a HTML_l5r5e_MultiSelectElement using provided configuration data.
* @param {FormInputConfig<string[]> & Omit<SelectInputConfig, "blank">} config
* @returns {L5r5eHtmlMultiSelectElement}
*/
static create(config) {
// Foundry creates either a select with tag multi-select or multi-checkboxes. We want a l5r5e-multi-select
// Copied the implementation from foundry.applications.fields.createMultiSelectInput with our required changes.
const groups = prepareSelectOptionGroups(config);
const element = document.createElement(L5r5eHtmlMultiSelectElement.tagName);
element.name = config.name;
foundry.applications.fields.setInputAttributes(element, config);
if (config.hideDisabledOptions) {
element.toggleAttribute("hidedisabledoptions", true);
}
//Setup the HTML
const select = document.createElement(L5r5eHtmlMultiSelectElement.tagName);
select.name = config.name;
foundry.applications.fields.setInputAttributes(select, config);
for (const group_entry of groups) {
let parent = select;
if (group_entry.group) {
parent = _appendOptgroupHtml(group_entry.group, select);
for (const groupEntry of groups) {
let parent = element;
if (groupEntry.group) {
parent = _appendOptgroup(groupEntry.group, element);
}
for (const option_entry of group_entry.options) {
_appendOptionHtml(option_entry, parent);
for (const groupOption of groupEntry.options){
_appendOption(groupOption, parent);
}
}
return select;
return element;
}
}
/** Stolen from foundry.applications.fields.prepareSelectOptionGroups: Needed to add support for tooltips
*
*/
/* -------------------------------------------- */
/* Module Helpers */
/* -------------------------------------------- */
function prepareSelectOptionGroups(config) {
const result = foundry.applications.fields.prepareSelectOptionGroups(config);
// Disable options based on input
config.options.filter((option) => option?.disabled || option?.tooltip).forEach((SpecialOption) => {
result.forEach((group) => {
group.options.forEach((option) => {
if (SpecialOption.value === option.value) {
option.disabled = SpecialOption.disabled;
option.tooltip = SpecialOption?.tooltip;
config.options.filter(option => option?.disabled || option?.tooltip).forEach(special => {
result.forEach(group => {
group.options.forEach(groupOption => {
if (groupOption.value === special.value) {
groupOption.disabled = special.disabled;
groupOption.tooltip = special.tooltip;
}
})
})
})
});
});
});
return result;
}
/** Stolen from foundry.applications.fields
* Create and append an optgroup element to a parent select.
* @param {string} label
* @param {HTMLSelectElement} parent
* @returns {HTMLOptGroupElement}
* @internal
*/
function _appendOptgroupHtml(label, parent) {
const optgroup = document.createElement("optgroup");
optgroup.label = label;
parent.appendChild(optgroup);
return optgroup;
function _appendOptgroup(label, parent) {
const element = document.createElement("optgroup");
element.label = label;
parent.appendChild(element);
return element;
}
/** Stolen from foundry.applications.fields
* Create and append an option element to a parent select or optgroup.
* @param {FormSelectOption} option
* @param {HTMLSelectElement|HTMLOptGroupElement} parent
* @internal
*/
function _appendOptionHtml(option, parent) {
function _appendOption(option, parent) {
const { value, label, selected, disabled, rule, tooltip } = option;
if ((value !== undefined) && (label !== undefined)) {
const option_html = document.createElement("option");
option_html.value = value;
option_html.innerText = label;
if (value !== undefined && label !== undefined) {
const element = document.createElement("option");
element.value = value;
element.innerText = label;
if (selected) {
option_html.toggleAttribute("selected", true);
element.toggleAttribute("selected", true);
}
if (disabled) {
option_html.toggleAttribute("disabled", true);
element.toggleAttribute("disabled", true);
}
if (tooltip) {
option_html.setAttribute("title", tooltip);
element.setAttribute("title", tooltip);
}
parent.appendChild(option_html);
parent.appendChild(element);
}
if (rule) {
parent.insertAdjacentHTML("beforeend", "<hr>");

View File

@@ -241,25 +241,24 @@ export const RegisterSettings = function () {
/* -------------------------------------- */
/* 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", {
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
});
});
};

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -53,6 +53,9 @@ $l5r5e-chat-color-roll: rgba(225, 215, 200, 0.75);
$l5r5e-chat-color-blind: transparent;
$l5r5e-chat-color-whisper: rgba(225, 200, 225, 0.75);
// Misc
$l5r5e-selection-circle-color: #8a1a00;
// -- Rings
// Earth
@@ -65,6 +68,7 @@ $l5r5e-water: rgb(95, 145, 155);
$l5r5e-fire: rgb(155, 115, 80);
// Void
$l5r5e-void: rgb(75, 70, 65);
$l5r5e-void-light: rgba(207,207,207,.8);
// -- Clans

View File

@@ -5,9 +5,55 @@
cursor: url("../assets/cursors/pointer.webp"), pointer;
}
// define a mixin
@mixin roll-effects-base {
clear: both;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2px 4px;
.effect-container {
border: 1px solid #5a6e5a;
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.4);
padding: 3px;
display: flex;
}
.effect-delete {
width: 16px;
height: 16px;
background-repeat: no-repeat;
background-size: contain;
text-align: end;
cursor: url("../assets/cursors/pointer.webp"), pointer;
}
.effect-icon {
width: 16px;
height: 16px;
background-repeat: no-repeat;
background-size: contain;
}
.effect-name {
vertical-align: top;
white-space: nowrap;
color: $white;
margin-left: 4px;
font-size: 14px;
line-height: 16px;
}
}
// Dice Picker
.dice-picker-dialog {
min-width: 35rem;
.effects {
@include roll-effects-base();
}
width: 35rem;
min-height: auto;
// Utility
* {
@@ -206,6 +252,12 @@
.profil {
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column; // stack profile ul and effects ul vertically
.effects {
@include roll-effects-base();
}
}
.dropbox {

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,8 @@
"changelog": "https://gitlab.com/teaml5r/l5r5e/-/blob/master/CHANGELOG.md",
"license": "https://gitlab.com/teaml5r/l5r5e/-/blob/master/LICENSE.md",
"manifest": "https://gitlab.com/teaml5r/l5r5e/-/raw/master/system/system.json",
"download": "https://gitlab.com/teaml5r/l5r5e/-/jobs/artifacts/v1.13.3/raw/l5r5e.zip?job=build",
"version": "1.13.3",
"download": "https://gitlab.com/teaml5r/l5r5e/-/jobs/artifacts/v1.13.4/raw/l5r5e.zip?job=build",
"version": "1.13.4",
"compatibility": {
"minimum": "13",
"verified": "13",
@@ -29,6 +29,10 @@
"name": "Carter",
"discord": "Carter#2703",
"url": "https://fr.tipeee.com/carter-foundryvtt"
},
{
"name": "Litasa",
"discord": "Litasa#3139"
}
],
"background": "systems/l5r5e/assets/l5r-header.webp",

View File

@@ -13,9 +13,9 @@
<div class="readiness">
<ul>
<li>
<label class="attribute-label-casualties">
<label class="attribute-label casualties">
<input name="system.battle_readiness.casualties_strength.value" type="number" value="{{data.system.battle_readiness.casualties_strength.value}}" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
<span class="attributes-buttons">
<span class="increment-control casualties">
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="casualties" data-value="1"></i>
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="casualties" data-value="-1"></i>
</span>
@@ -23,15 +23,15 @@
<strong>{{localize 'l5r5e.army.battle_readiness.casualties'}}</strong>
</li>
<li>
<label class="attribute-label-strength">
<label class="attribute-label strength">
<input name="system.battle_readiness.casualties_strength.max" type="number" value="{{data.system.battle_readiness.casualties_strength.max}}" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
</label>
<strong>{{localize 'l5r5e.army.battle_readiness.strength'}}</strong>
</li>
<li>
<label class="attribute-label-panic">
<label class="attribute-label panic">
<input name="system.battle_readiness.panic_discipline.value" type="number" value="{{data.system.battle_readiness.panic_discipline.value}}" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
<span class="attributes-buttons">
<span class="increment-control panic">
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="panic" data-value="1"></i>
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="panic" data-value="-1"></i>
</span>
@@ -39,7 +39,7 @@
<strong>{{localize 'l5r5e.army.battle_readiness.panic'}}</strong>
</li>
<li>
<label class="attribute-label-discipline">
<label class="attribute-label discipline">
<input name="system.battle_readiness.panic_discipline.max" type="number" value="{{data.system.battle_readiness.panic_discipline.max}}" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
</label>
<strong>{{localize 'l5r5e.army.battle_readiness.discipline'}}</strong>

View File

@@ -7,7 +7,7 @@
<label class="attribute-label">
<strong>{{localize 'l5r5e.attributes.fatigue'}}</strong>
<input class="centered-input select-on-focus" type="number" name="system.fatigue.value" value="{{data.system.fatigue.value}}" data-dtype="Number" min="0" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
<span class="attributes-buttons">
<span class="increment-control fatigue">
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="fatigue" data-value="1"></i>
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="fatigue" data-value="-1"></i>
</span>
@@ -22,7 +22,7 @@
<label class="attribute-label">
<strong>{{localize 'l5r5e.attributes.strife'}}</strong>
<input class="centered-input select-on-focus" type="number" name="system.strife.value" value="{{data.system.strife.value}}" data-dtype="Number" min="0" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
<span class="attributes-buttons">
<span class="increment-control strife">
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="strife" data-value="1"></i>
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="strife" data-value="-1"></i>
</span>

View File

@@ -3,6 +3,10 @@
<label class="attribute-label">
{{localize 'l5r5e.advancements.total'}}
<input class="centered-input select-on-focus" type="number" name="system.xp_total" value="{{data.system.xp_total}}" data-dtype="Number" min="0" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
<span class="increment-control xp">
<i class="xp-control pointer-choice fa fa-plus-square" data-type="xp" data-value="1"></i>
<i class="xp-control pointer-choice fa fa-minus-square" data-type="xp" data-value="-1"></i>
</span>
</label>
<label class="attribute-label">
{{localize 'l5r5e.advancements.spent'}}

View File

@@ -1,25 +1,25 @@
<fieldset class="money money-wrapper">
<legend class="section-header">{{localize 'l5r5e.money.title'}}</legend>
<label>
<label class="attribute-label money">
{{localize 'l5r5e.money.koku'}}
<input name="system.money.koku" type="number" value="{{data.system.money.koku}}" data-dtype="Number" min="0" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
<span class="money-buttons">
<span class="increment-control money">
<i class="money-control pointer-choice fa fa-plus-square" data-type="koku" data-value="1"></i>
<i class="money-control pointer-choice fa fa-minus-square" data-type="koku" data-value="-1"></i>
</span>
</label>
<label>
<label class="attribute-label money">
{{localize 'l5r5e.money.bu'}}
<input name="system.money.bu" type="number" value="{{data.system.money.bu}}" data-dtype="Number" min="0" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
<span class="money-buttons">
<span class="increment-control money">
<i class="money-control pointer-choice fa fa-plus-square" data-type="bu" data-value="1"></i>
<i class="money-control pointer-choice fa fa-minus-square" data-type="bu" data-value="-1"></i>
</span>
</label>
<label>
<label class="attribute-label money">
{{localize 'l5r5e.money.zeni'}}
<input name="system.money.zeni" type="number" value="{{data.system.money.zeni}}" data-dtype="Number" min="0" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
<span class="money-buttons">
<span class="increment-control money">
<i class="money-control pointer-choice fa fa-plus-square" data-type="zeni" data-value="1"></i>
<i class="money-control pointer-choice fa fa-minus-square" data-type="zeni" data-value="-1"></i>
</span>

View File

@@ -2,18 +2,30 @@
<li>
<label class="attribute-label centered-input">
<strong>{{localize 'l5r5e.social.honor'}}</strong>
<span class="increment-control honor">
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="honor" data-value="1"></i>
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="honor" data-value="-1"></i>
</span>
<input class="centered-input select-on-focus" type="number" name="system.social.honor" value="{{data.system.social.honor}}" data-dtype="Number" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
</label>
</li>
<li>
<label class="attribute-label centered-input">
<strong>{{localize 'l5r5e.social.glory'}}</strong>
<span class="increment-control glory">
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="glory" data-value="1"></i>
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="glory" data-value="-1"></i>
</span>
<input class="centered-input select-on-focus" type="number" name="system.social.glory" value="{{data.system.social.glory}}" data-dtype="Number" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
</label>
</li>
<li>
<label class="attribute-label centered-input">
<strong>{{localize 'l5r5e.social.status'}}</strong>
<span class="increment-control status">
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="status" data-value="1"></i>
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="status" data-value="-1"></i>
</span>
<input class="centered-input select-on-focus" type="number" name="system.social.status" value="{{data.system.social.status}}" data-dtype="Number" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
</label>
</li>

View File

@@ -7,7 +7,7 @@
<label class="attribute-label">
<strong>{{localize 'l5r5e.attributes.fatigue'}}</strong>
<input class="centered-input select-on-focus" type="number" name="system.fatigue.value" value="{{data.system.fatigue.value}}" data-dtype="Number" min="0" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
<span class="attributes-buttons">
<span class="increment-control fatigue">
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="fatigue" data-value="1"></i>
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="fatigue" data-value="-1"></i>
</span>
@@ -22,7 +22,7 @@
<label class="attribute-label">
<strong>{{localize 'l5r5e.attributes.strife'}}</strong>
<input class="centered-input select-on-focus" type="number" name="system.strife.value" value="{{data.system.strife.value}}" data-dtype="Number" min="0" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
<span class="attributes-buttons">
<span class="increment-control strife">
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="strife" data-value="1"></i>
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="strife" data-value="-1"></i>
</span>

View File

@@ -2,18 +2,30 @@
<li>
<label class="attribute-label centered-input">
<strong>{{localize 'l5r5e.social.honor'}}</strong>
<span class="increment-control honor">
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="honor" data-value="1"></i>
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="honor" data-value="-1"></i>
</span>
<input class="centered-input select-on-focus" type="number" name="system.social.honor" value="{{data.system.social.honor}}" data-dtype="Number" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
</label>
</li>
<li>
<label class="attribute-label centered-input">
<strong>{{localize 'l5r5e.social.glory'}}</strong>
<span class="increment-control glory">
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="glory" data-value="1"></i>
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="glory" data-value="-1"></i>
</span>
<input class="centered-input select-on-focus" type="number" name="system.social.glory" value="{{data.system.social.glory}}" data-dtype="Number" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
</label>
</li>
<li>
<label class="attribute-label centered-input">
<strong>{{localize 'l5r5e.social.status'}}</strong>
<span class="increment-control status">
<i class="addsub-control pointer-choice fa fa-plus-square" data-type="status" data-value="1"></i>
<i class="addsub-control pointer-choice fa fa-minus-square" data-type="status" data-value="-1"></i>
</span>
<input class="centered-input select-on-focus" type="number" name="system.social.status" value="{{data.system.social.status}}" data-dtype="Number" placeholder="0" {{^if data.editable_not_soft_locked}}disabled{{/if}}/>
</label>
</li>

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

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

View File

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

View File

@@ -1,19 +1,19 @@
<form class="l5r5e gm-toolbox" autocomplete="off">
<ul class="gm-tools-container">
<li class="faded-ui gm_monitor" data-action="open_gm_monitor" title="{{localize 'l5r5e.gm.monitor.title'}}">
<li class="gm_monitor" data-action="open_gm_monitor" title="{{localize 'l5r5e.gm.monitor.title'}}">
<i class="fas fa-table"></i>
</li>
<li class="difficulty_hidden" data-action="toggle_hide_difficulty" title="{{localize 'l5r5e.gm.toolbox.difficulty_hidden'}}">
<i class="faded-ui fa fa-eye{{#if difficultyHidden}}-slash{{/if}}"></i>
<strong class="faded-ui difficulty" data-action="change_difficulty" title="{{localize 'l5r5e.gm.toolbox.difficulty'}}">{{difficulty}}</strong>
<i class="fa fa-eye{{#if difficultyHidden}}-slash{{/if}}"></i>
<strong class="difficulty" data-action="change_difficulty" title="{{localize 'l5r5e.gm.toolbox.difficulty'}}">{{difficulty}}</strong>
</li>
<li class="faded-ui gm_actor_updates reset_void" data-action="reset_void" title="{{localize 'l5r5e.gm.toolbox.reset_void'}}">
<li class="gm_actor_updates reset_void" data-action="reset_void" title="{{localize 'l5r5e.gm.toolbox.reset_void'}}">
<i class="fas fa-podcast"></i>
</li>
<li class="faded-ui gm_actor_updates sleep" data-action="sleep" title="{{localize 'l5r5e.gm.toolbox.sleep'}}">
<li class="gm_actor_updates sleep" data-action="sleep" title="{{localize 'l5r5e.gm.toolbox.sleep'}}">
<i class="fa fa-bed"></i>
</li>
<li class="faded-ui gm_actor_updates scene_end" data-action="scene_end" title="{{localize 'l5r5e.gm.toolbox.scene_end'}}">
<li class="gm_actor_updates scene_end" data-action="scene_end" title="{{localize 'l5r5e.gm.toolbox.scene_end'}}">
<i class="fas fa-star-half-alt"></i>
</li>
</ul>

View File

@@ -1,7 +1,9 @@
# DicePicker (DP)
The DicePicker is the entry point to any L5R roll (but chat command).
## Usage example
```js
new game.l5r5e.DicePickerDialog({
skillId: 'aesthetics',
@@ -11,26 +13,27 @@ new game.l5r5e.DicePickerDialog({
```
## Constructor Options
| Property | Type | Notes / Examples |
|------------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------|
| actor | Actor | Any `Actor` object instance.<br>ex : `game.user.character`, `canvas.tokens.controlled[0].actor` |
| actorId | string | This is the `id` not the `uuid` of an actor.<br>ex : "AbYgKrNwWeAxa9jT" |
| actorName | string | Careful this is case-sensitive.<br>ex : "Isawa Aki" |
| 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.
For `actor*` properties, the resolution is in this order :
1. `option.actor`
2. `option.actorId`
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.
Usage :
```js
new RollnKeepDialog(messageId).render(true);
```

View File

@@ -1,7 +1,9 @@
# Roll
The roll use the `RollL5r5e` class, who store a lot of additional variables.
Here is a view of `<roll>.l5r5e` properties :
Here is a view of `<roll>.l5r5e` properties:
```js
actor: null, // actor instance
dicesTypes: {

View File

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

View File

@@ -1,52 +1,56 @@
# Sockets API
Here is the list of Socket's methods, most of them is design to be use internally by the system, but you can find some useful for your projects.
Here is the list of Socket's methods, most of them are designed to be use internally by the system, but you can find some useful ones for your projects.
## deleteChatMessage
Delete ChatMessage by his `id`, the GM permission is required, so a GM need to be connected for this to work. Used in RnK.
Delete ChatMessage by his `id`, the GM permission is required, so a GM needs to be connected for this to work. Used in RnK.
Usage :
```js
game.l5r5e.sockets.deleteChatMessage(messageId);
```
## refreshAppId
Refresh an application windows by his `id` (not `appId`). Used in RnK.
<br>Ex : `l5r5e-twenty-questions-dialog-kZHczAFghMNYFRWe`, not `65`.
Usage :
```js
game.l5r5e.sockets.refreshAppId(applicationId);
```
## updateMessageIdAndRefresh
Change the message in the selected application windows, and re-render the application to force the refresh. Used in RnK.
Usage :
```js
game.l5r5e.sockets.refreshAppId(applicationId, messageId);
```
## openDicePicker
_Added in v1.9.0_
Remotely open the DicePicker (DP) on targeted Users/Actors if they are active users. Used in initiative roll.
Arguments :
| Property | Type | Notes / Examples |
|-----------|---------|----------------------------------------------------------------------------|
| users | User[] | Users list to trigger the DP (will be reduce to `id` for network perf.) |
| Property | Type | Notes / Examples |
|:----------|:--------|:-----------------|
| users | User[] | Users list to trigger the DP (will be reduce to `id` for network perf.) |
| actors | Actor[] | Actors list to trigger the DP (will be reduce to `uuid` for network perf.) |
| dpOptions | Object | Any [DicePickerDialog.options](dicepicker.md#constructor-options) |
| dpOptions | Object | Any [DicePickerDialog.options](dicepicker.md#constructor-options) |
### Examples
#### Fitness skill roll for the all combat targets
```js
game.l5r5e.sockets.openDicePicker({
actors: Array.from(game.user.targets).map(t => t.document.actor),
@@ -58,6 +62,7 @@ game.l5r5e.sockets.openDicePicker({
```
#### Initiative roll (skirmish) for all player's character who are in combat tracker
```js
game.l5r5e.sockets.openDicePicker({
actors: game.combat.combatants.filter(c => c.hasPlayerOwner && !c.isDefeated && !c.initiative).map(c => c.actor),
@@ -70,6 +75,7 @@ game.l5r5e.sockets.openDicePicker({
```
#### Melee skill roll with "fire" ring, pre-selected for all the selected tokens
```js
game.l5r5e.sockets.openDicePicker({
actors: canvas.tokens.controlled.map(t => t.actor),
@@ -82,6 +88,7 @@ game.l5r5e.sockets.openDicePicker({
```
#### Skill roll with skill list for all active players (but GM)
```js
game.l5r5e.sockets.openDicePicker({
users: game.users.players.filter(u => u.active && u.hasPlayerOwner),

View File

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

View File

@@ -1,21 +1,24 @@
# System helping (Contribute)
## Rules
You are free to contribute and propose corrections, modifications after fork. Try to respect theses rules:
- Make sure you are up-to-date with the referent branch (most of the time the `dev` branch).
- Clear and precise commit messages allow a quick review of the code.
- If possible, limit yourself to one Feature per Merge request so as not to block the process.
You are free to contribute and propose corrections and modifications after forking the repository. Try to respect these rules:
- Make sure you are up-to-date with the reference branch (generally this is the `dev` branch).
- Clear and precise commit messages allow a quick review of the code. For fixes, recommend the testing steps for a reviewer to properly test the fix.
- If possible, limit yourself to one Feature per Merge request to avoid slowing down the review process with multiple feature commits.
## Dev install
1. Clone the repository.
2. Use `npm ci` to install the dependence.
3. Create a link from `<repo>/system` to your foundry system data (by default `%localappdata%/FoundryVTT/data/systems/l5r5e`).
## Dev Install
1. Clone the repository to your local machine.
2. Ensure that [Node.js](https://nodejs.org/en/download/) is installed.
3. Use `npm ci` to install the relevant dependencies.
4. Create a link from `<repo>/system` to your foundry system data (by default `%localappdata%/FoundryVTT/data/systems/l5r5e`).
Windows example (modify the target and source directories, and run this in administrator):
Windows example (modify the target and source directories, and run this in administrator) :
```bash
mklink /D /J "%localappdata%/FoundryVTT/data/systems/l5r5e" "D:/Projects/FVTT/l5r5e/system"
```
## Compiling SCSS
1. Run `npm run watch` to watch and compile the `scss` files.
1. Run `npm run watch` to watch and compile the `*.scss` files.

View File

@@ -1,16 +1,18 @@
# L5R5e System Wiki
## For users
- [Installation and modules](users/install.md)
- [Updating - Bests practices](users/updating.md)
- [Basic usage and filling sheets](users/basic-usage.md)
- [Dice Rolling](users/dice.md)
- [Symbols replacement list](users/symbols.md)
- [Advanced : Techniques skill and difficulty syntaxe](users/techniques-syntaxe.md)
- [Advanced : Techniques skill and difficulty syntaxe](users/techniques-syntax.md)
- [Advanced : Custom Compendiums](users/custom-compendiums.md)
- [Advanced : Using CUB for modifiers](users/cub-modifiers.md)
## For developers
- [System helping (Contribute)](dev/system-helping.md)
- [Snippets](dev/snippets.md)
- [Sockets API](dev/sockets.md)

View File

@@ -1,7 +1,35 @@
# Basic usage and filling sheets
Mostly all the interactions are done with drag-n-drop.
Most interactions are done via drag and drop. You will need to activate the appropriate `Techniques` to allow you to place them in the sheet slots.
## Creating a Character (PC)
Go in actor tab, and create a new `character`.
> TODO
In the `Actor` tab, you can create a new `Character` by clicking the add actor button and using the `Player Character` sheet format. Generally, the Game Master (GM) will have to complete this step for you and assign you ownership before you can modify the character sheet.
1. You can build the character manually following the game system 20 questions sheet *or* you can use the `20Q` link at the top of the sheet and configure it with the 20 question output. Most of the steps below should ben completed automatically if you used the `20Q` feature.
2. Fill in your `Honor`, `Glory` and `Status` stats. Increase your rings to the appropriate level for your character build. Your `Endurance`, `Composure`, `Focus` and `Vigilance` stats should all auto-calculate based on your ring bonus.
3. Add any additional skill points in each of the `Artisan`, `Martial`, `Scholar`, `Social` or `Trade` skill groups.
4. Select your allowed technique categories. You can select new techniques by dragging and dropping them from the L5R5e system compendiums under `L5R5e` > `Techniques` > selection of the appropriate technique category.
5. Apply your school ability from the `School Abilities` compendium and a `Signature Scroll`, if appropriate.
6. Ensure your `Ninjo` and `Giri` have been defined. Select `Distinctions`, `Passions` and `Bonds` from the `L5R5e` > `Character Related` section of the compendium.
7. Define the `Paramount` and `Less Significant` Bushido aspects of your clan and any additional notes or character description in the respective fields.
8. In the `Inventory` screen, you can drag elements from the `L5R5e` > `Objects related` section for `Items`, `Armors` and `Weapons` to add them to your character sheet.
> **Note**: Several of the default weapons and armors that are on your characters may not be available in the compendium. Simply drag the closest description item you can find and rename and change the stats appropriately once it's been placed in your character sheet.
9. Add `Item Patterns` from the `Item Patterns` compendium, if appropriate.
## Creating an NPC
Non-player Characters (NPCs) come in two flavors: minions and adversaries. While they differ in L5R in terms of power and influence on the narrative, they are basically built the same way in the Non-Player Character sheet.
1. Define the `Combat` and `Intrigue` difficulty, as appropriate, at the top of the sheet.
2. Similar to the PCs, you will define `Honor`, `Glory`, `Status`, and Ring values for each character. Unlike players, you will need to manually define `Endurance`, `Composure`, `Focus` and `Vigilance`.
3. Determine the demeanor for the character, and fill that in the `Demeanor` section. Modify the ring values based on the demeanor below the box. Acceptable values are an integer (i.e., 1, 2, 3, etc.) or an integer with a negative number (i.e., -1, -2, -3, etc.). Do not use `+` symbols in these fields.
4. Define the general skill values in the `Artisan`, `Martial`, `Scholar`, `Social`, and `Trade` boxes. Unlike player characters, the skills are generalized for NPCs.
5. The `Narrative` tab is filled out similarly to players. Note that most of the Distinctions and Passions of NPCs do not have a compendium equivalent. You can either build a custom compendium of these or manually define them by click the `+` button above each section to add and modify them.
6. The inventory tab is filled out in much the same way as players.
## Creating an Army
> Todo

View File

@@ -1,18 +1,20 @@
# Using CUB for Modifiers
> ⚠ ***Warning***: This module is outdated and the project has been abandoned. If you are using more current versions of Foundry (v11+), it is **not** recommended that you continue to use *Combat Utility Belt*.
# Using Combat Utility Belt (CUB) for Modifiers
> ⚠ The module [Combat Utility Belt](https://foundryvtt.com/packages/combat-utility-belt) is required.
## Attributes modifiers
Replace `<attribute>` with actual attribute (i.e. `endurance`, `vigilance`, `focus`, `composure`) and `<number>` with actual number to be added.
Replace `<attribute>` with the actual system attribute (i.e. `endurance`, `vigilance`, `focus`, `composure`) and `<number>` with actual number to be added.
When setup in CUB this would modify PC derived attributes to increase or reduce them by the number given.
Allows automating certain invocations and item effects (such as the cursed Kama from Sins of Regret supplement).
When setup in CUB this would modify PC-derived attributes to increase or reduce them by the number given.
This allows automating certain invocations and item effects (such as the cursed Kama from Sins of Regret supplement).
### For `character` type
Syntaxe:
Syntax:
> system.modifiers.character.`<attribute>` += `<number>`
Examples:
@@ -21,29 +23,29 @@ Examples:
### For `adversary` or `minion` types
Syntaxe:
Syntax:
> system.`<attribute>` += `<number>`
Exemples:
Examples:
> system.vigilance += 1 // add 1
> <br>system.composure += -2 // remove 2
## Rings/Skills modifiers
Both PCs and NPCs can have their skills and rings increased as well by conditions (should you wish to ignore some of the RAW).
Syntaxe:
Syntax:
> system.rings.`<ring>` += `<number>`
> <br>system.skills.`<skillGroup>`.`<skill>` += `<number>` // for PCs
> <br>system.skills.`<skillGroup>` += `<number>` // for NPCs
Exemples:
Examples:
> system.rings.earth += 1
> <br>system.skills.artisan.aesthetics += 1 // for PCs
> <br>system.skills.martial += -1 // for NPCs
The above need to be setup as conditions using CUB at the moment so that they can be added/removed as required.
The above has to be set up as conditions using CUB at the moment so that they can be added/removed as required.
Regarding skills and rings modifiers, I believe you would need to remove them temporarily for advancements as it might cause extra XP to be spent, but yet to test it fully.
Regarding skills and rings modifiers, you may need to remove them temporarily for advancements as it might cause extra XP to be spent. This has not been fully tested.

View File

@@ -1,12 +1,11 @@
# Compendiums
**Never directly edit the system compendiums**.
They will be erased anytime you update the system.
The best option to keep the links between items and properties for example, is to fill a custom compendium using Babele.
This way, the system compendiums will be overridden by this module.
**Never directly edit the system compendiums**. They will be erased anytime you update the system.
The best option to keep the links between items and properties, for example, is to fill out a custom compendium using Babele. This will prevent changes you make to system compendiums from being overridden by game system updates.
## Using the Custom Compendiums Maker (recommended)
Here is a quick guide to fill the compendiums with Custom Compendium Maker.
1. Go to the [Custom Compendiums Maker Website](https://vly.yn.lu/l5r5e-custom-compendium-maker/)
@@ -14,7 +13,7 @@ Here is a quick guide to fill the compendiums with Custom Compendium Maker.
![Download](img/ccm_usage_dl.png)
3. Open the template file with any software who can open and write an Excel sheet (.xlsx).
3. Open the template file with any software that can modify an Excel sheet (.xlsx).
4. Check instruction on `Infos` sheet.
5. Go into the `Configuration` sheet, and changes the values in the `Values` column.<br>If your language is `English` you'll probably only need to change the `Author` value.
@@ -24,8 +23,8 @@ Here is a quick guide to fill the compendiums with Custom Compendium Maker.
![Template Techniques](img/ccm_excel_tech.png)
7. Regularly save the file !
8. When it's done, visit the website again and follow the instructions.
7. Make sure you save the file often!
8. When it's done, visit the website again and follow the instructions:
![Options](img/ccm_usage_options.png)
@@ -39,21 +38,21 @@ Here is a quick guide to fill the compendiums with Custom Compendium Maker.
![FVTT CCM](img/ccm_archive_extracted.png)
11. Open FoundryVTT and load an L5R5e World.
12. Make sure to have all these modules enabled :
12. Make sure to have all these modules enabled:
- Babele
- L5R5e Custom Compendiums
- LibWrapper
![FVTT Loaded](img/ccm_foundry_module_manager.png)
13. Open an item you know you had added a description to see the result :
13. Open an item you know you had added a description to see the result:
![FVTT Tech](img/ccm_foundry_tech_filled.png)
14. Done, have a good time playing L5R !
14. Done, have a good time playing L5R!
## Using the Custom Compendiums Module
You will need to manually download the following module, and edit json files.
You will need to manually download the following module and edit json files.
Please follow the instructions on :
> https://gitlab.com/teaml5r/l5r5e-custom-compendiums

View File

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

View File

@@ -1,6 +1,8 @@
# Symbols replacement list
In sheets or journals, you can use these tags to use symbols :
```
In sheets or journals, you can use these tags to embed a symbol in the text:
```md
Dice symbols : (op) (su) (ex) (st) (skill) (ring)
Rings : (earth) (water) (fire) (air) (void)
Tech: (kiho) (maho) (ninjutsu) (ritual) (shuji) (invocation) (kata) (prereq) (inversion) (mantra)
@@ -10,4 +12,4 @@ Others : (courtier) (bushi) (shugenja)
Result :
![](img/symbols.png)
![Inline Dice](img/symbols.png)

View File

@@ -1,10 +1,13 @@
# Techniques skill and difficulty syntaxe
# Techniques skill and difficulty syntax
On the Technique sheets, you will find two fields `Difficulty` and `Skill`.
These fields have special constraints, you will find theirs rules below.
These fields have special constraints, with the following rules listed below:
## Difficulty
Can be :
Valid values for this field are:
- A integer number : `1` to `9`.
- Or specific syntax "@`S`:`prop1`" or "@`T`:`prop1`|`max`" or "@`T`:`prop1`|`max`(`prop2`)" :
- `@` fixed, trigger the parser
@@ -18,9 +21,10 @@ Can be :
- `@T:vigilance|min` : Difficulty will be the `vigilance` from the target with the minimum vigilance (implicit) value. it's the same to wrote `@T:vigilance|min(vigilance)`.
- `@T:vigilance|max(statusRank)` : Difficulty will be the `vigilance` from the target with the maximum `statusRank` value.
## Skill
Can be :
Valid values for this field are:
- Any `Skill` id : `melee`, `fitness`...
- Any `SkillCategory` id : `scholar`, `martial`...
- Or both in list, coma separated.

View File

@@ -1,4 +1,10 @@
# Updating - Bests practices
- Anytime you update to a major version make a backup of foundry's data directory (default : `%localappdata%/FoundryVTT/data/`).
- Take time to upgrading to a major version (ex FoundryVTT v9->v10).<br>A lot of bugs can be on firsts patchs, and a lots of systems/modules won't upgrade fast and will be incompatible or not tested (a lot of us do it only on our free time).<br>If you need some timing windows: let at least 2 weeks to 1 month.
- Anytime you update to a major version:
- **Foundry**: make a backup of foundry's data directory (default : `%localappdata%/FoundryVTT/data/`).
- **Forge**: you will be prompted on Foundry upgrades (both major and minor) to download a local copy of the world for backup. It is recommended you complete this step to avoid data loss.
- It is recommended that you wait for some time before a major upgrade (ex FoundryVTT v9 -> v10) when a new version is released. This are a number of reasons to avoid immediate upgrades:
- Several modules your world uses will have a lot of bugs introduced that aren't fully corrected in the first few patches.
- Several systems/modules won't update quickly, will be incompatible with the current Foundry version, or left untested (the developers for a many of modules and systems only make updates in their free time).
- A solid recommendation for update delay should be somewhere in the range of 2 weeks to 1 month, if not longer. Generally, this is less of a concern with minor upgrades which don't change core systems in Foundry.
- Make sure you check the compatibility of your world's modules with the new versions and potentially disable certain modules if there is little chance of a near-term update. A good tool to use for managing modules profiles is [Module Profiles](https://foundryvtt.com/packages/module-profiles) to ensure that game-breaking modules can be deactivated by having a master profile.