Compare commits

..

29 Commits

Author SHA1 Message Date
55d7e401c1 Finalize aappv2 data models migration
All checks were successful
Release Creation / build (release) Successful in 1m3s
2026-02-27 14:38:33 +01:00
5e2916202e Finalize aappv2 data models migration 2026-02-27 14:37:22 +01:00
c45837ea31 Finalize aappv2 data models migration 2026-02-27 14:36:54 +01:00
8735b7e4a4 Fix basic system stuff 2026-02-26 21:25:05 +01:00
34db8695d7 Fix tirage de cartes 2025-09-29 19:51:39 +02:00
a109fd6acb Fix tirage de cartes 2025-09-29 19:49:02 +02:00
d647fcc35e Foundry v13 migration 2025-05-01 08:48:02 +02:00
342f9c2342 Fix waring about grid 2025-02-14 13:37:51 +01:00
cd14db85cc Fix v11/v12 2024-05-02 09:29:42 +02:00
ec06f0fdcb Update pour v11/v12 et correction d'un bug sur les defenses 2024-04-26 18:35:39 +02:00
234bd44742 Enhance stats 2024-02-08 12:51:06 +01:00
e0df1f1ff5 Fix CSS pour confrontation et CSS pour DiceTray 2023-10-26 09:25:07 +02:00
2c92dd6ef9 Fix notes + CSS 2023-10-25 00:00:30 +02:00
8af5851246 v10/v11 compatibility 2023-05-25 08:04:31 +02:00
14b536cc52 Fix sur ajout tarots 2023-05-08 23:24:05 +02:00
9944ebe64d Fix sur armes et affichage 2023-03-08 20:24:19 +01:00
165c836f39 Fix sur tarots et message 2023-02-28 13:06:46 +01:00
cb8e70c6c1 Ajout zone libre, macros et tirage de carte 2023-02-26 15:38:44 +01:00
b2a9d8cb75 Gestion armes 2023-02-25 09:38:35 +01:00
6d75c8532c Update README 2023-02-24 15:28:35 +01:00
dce8ad025b Update README 2023-02-24 15:27:49 +01:00
6e4cd71b99 Minot fixes 2023-02-22 23:08:03 +01:00
e62480efb0 Fix tarots 2023-02-20 08:17:21 +01:00
c5509143b1 Sort, confrontation, update tarots, etc 2023-02-08 17:51:16 +01:00
e146c6ba5b Tirage des tarots 2023-02-07 20:21:50 +01:00
04039513bc Tirage des tarots 2023-02-07 19:55:33 +01:00
1923a63ebf Update tarot management 2023-02-07 15:36:06 +01:00
06537cbcd9 Ajout elements bio et fix mineurs 2023-02-06 16:08:11 +01:00
925f15627c Ajout elements bio et fix mineurs 2023-02-06 16:05:59 +01:00
149 changed files with 11640 additions and 1099 deletions

View File

@@ -0,0 +1,62 @@
name: Release Creation
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo "💡 The ${{ gitea.repository }} repository will be cloned to the runner."
- uses: RouxAntoine/checkout@v3.5.4
# get part of the tag after the `v`
- name: Extract tag version number
id: get_version
uses: battila7/get-version-action@v2
# Substitute the Manifest and Download URLs in the system.json
- name: Substitute Manifest and Download Links For Versioned Ones
id: sub_manifest_link_version
uses: microsoft/variable-substitution@v1
with:
files: 'system.json'
env:
version: ${{steps.get_version.outputs.version-without-v}}
url: https://www.uberwald.me/gitea/${{gitea.repository}}
manifest: https://www.uberwald.me/gitea/public/fvtt-malefices/releases/download/latest/system.json
download: https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{github.event.release.tag_name}}/fvtt-malefices.zip
# Create a zip file with all files required by the system to add to the release
- run: |
apt update -y
apt install -y zip
- run: zip -r ./fvtt-malefices.zip system.json README.md fonts/ images/ lang/ modules/ packs/ styles/ templates/
- name: setup go
uses: https://github.com/actions/setup-go@v4
with:
go-version: '>=1.20.1'
- name: Use Go Action
id: use-go-action
uses: https://gitea.com/actions/release-action@main
with:
files: |-
./fvtt-malefices.zip
system.json
api_key: '${{secrets.ALLOW_PUSH_RELEASE}}'
- name: Publish to Foundry server
uses: https://github.com/djlechuck/foundryvtt-publish-package-action@v1
with:
token: ${{ secrets.FOUNDRYVTT_RELEASE_TOKEN }}
id: 'fvtt-malefices'
version: ${{github.event.release.tag_name}}
manifest: 'https://www.uberwald.me/gitea/public/fvtt-malefices/releases/download/latest/system.json'
notes: 'https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{github.event.release.tag_name}}/fvtt-malefices.zip'
compatibility-minimum: '11'
compatibility-verified: '13'

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules/

View File

@@ -1,5 +1,7 @@
# Système Foundry pour Maléfices (French RPG, Arkhane Asylum Publishing)
[Vue du système](https://www.lahiette.com/leratierbretonnien/wp-content/uploads/2023/02/malefices_snapshot.webp)
## EN
Unofficial system for Maléfices v4 (French version from Arkhane Asylum Publishing).

27
gulpfile.js Normal file
View File

@@ -0,0 +1,27 @@
const gulp = require('gulp');
const less = require('gulp-less');
const sourcemaps = require('gulp-sourcemaps');
const rename = require('gulp-rename');
// Compile LESS to CSS
function styles() {
return gulp.src('less/malefices.less')
.pipe(sourcemaps.init())
.pipe(less())
.pipe(rename('simple.css'))
.pipe(sourcemaps.write('.', { mapFile: () => 'simple.css.map' }))
.pipe(gulp.dest('styles/'));
}
// Watch files
function watchFiles() {
gulp.watch('less/**/*.less', styles);
}
const build = gulp.series(styles);
const watch = gulp.series(build, watchFiles);
exports.styles = styles;
exports.build = build;
exports.watch = watch;
exports.default = build;

6
images/icons/.directory Normal file
View File

@@ -0,0 +1,6 @@
[Dolphin]
SortRole=modificationtime
Timestamp=2023,2,26,15,32,34.892
Version=4
ViewMode=1
VisibleRoles=Details_text,Details_size,Details_modificationtime,Details_creationtime,CustomizedDetails

BIN
images/icons/archetype.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
images/icons/resume.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
images/icons/sortilege.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
images/icons/tarot.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
images/icons/tirage.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
images/icons/tirer.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
images/icons/wisdom.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 453 KiB

After

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 532 KiB

After

Width:  |  Height:  |  Size: 488 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 534 KiB

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 590 KiB

After

Width:  |  Height:  |  Size: 538 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 572 KiB

After

Width:  |  Height:  |  Size: 525 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 545 KiB

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 598 KiB

After

Width:  |  Height:  |  Size: 549 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 KiB

After

Width:  |  Height:  |  Size: 473 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 KiB

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 522 KiB

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 584 KiB

After

Width:  |  Height:  |  Size: 538 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 591 KiB

After

Width:  |  Height:  |  Size: 546 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 504 KiB

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 584 KiB

After

Width:  |  Height:  |  Size: 537 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 583 KiB

After

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 563 KiB

After

Width:  |  Height:  |  Size: 527 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 581 KiB

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 590 KiB

After

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 619 KiB

After

Width:  |  Height:  |  Size: 571 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 564 KiB

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 KiB

After

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 KiB

After

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

View File

@@ -1,22 +1,15 @@
{
"ACTOR": {
"TypeCharacter": "Character",
"TypeNpc": "NPC"
"TYPES": {
"Actor": {
"personnage" : "Personnage"
},
"ITEM": {
"TypeWeapon": "Weapon",
"TypeShield": "Shield",
"TypeArmor": "Armor",
"TypeSpell": "Spell",
"TypeModule": "Module",
"TypeMoney": "Money",
"TypeEquipment": "Equipment",
"TypeAction": "Action",
"TypeFreeaction": "Free Action",
"TypeReaction": "Reaction",
"TypeStance": "Stance",
"TypeTrait": "Trait",
"TypeCondition": "Condition",
"TypeCraftingskill": "Crafting Skill"
"Item": {
"arme" : "Arme",
"equipement" : "Equipement",
"tarot" : "Tarot",
"elementbio" : "Elément Biographique",
"archetype" : "Archetype",
"sortilege" : "Sortilège"
}
}
}

235
less/actor-sheet.less Normal file
View File

@@ -0,0 +1,235 @@
/* ===================================================================
AppV2 Actor Sheets
=================================================================== */
/* NOTE: DocumentSheetV2 uses tag:"form" — the APPLICATION ROOT is the <form>.
The DOM is: form.application > section.window-content > section.editable > ...
There is NO <form> child inside window-content. */
.fvtt-malefices.actor {
/* window-content: background and remove Foundry's default padding.
Foundry AppV2 already sets: display:flex flex-flow:column overflow:hidden */
.window-content {
padding: 0;
background: @bg-sheet;
color: @color-text;
font-size: 0.8rem;
}
/* All sections inside actor fill their flex container and form a flex column.
This covers: section.editable (template root) and section.sheet-body. */
section {
height: 100%;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
}
/* sheet-body: scroll container instead of clipping */
section.sheet-body {
overflow-y: auto;
overflow-x: hidden;
padding: 0.25rem 0.5rem;
}
/* Override legacy fixed 210px height — shrink to fit content */
.sheet-header {
flex: 0 0 auto;
overflow: visible;
}
/* Override legacy "height: 100%" on .tab divs so their content
can overflow sheet-body and trigger the scrollbar;
hide all tabs, show only the active one */
.tab[data-tab] {
height: auto;
display: none;
&.active {
display: block;
}
}
/* Tab navigation */
nav.tabs {
display: flex;
flex: 0 0 auto;
font-family: @font-rivanna;
font-size: @tab-font-size;
font-weight: bold;
height: @tab-height;
margin: 0;
padding: 0 0 0 0.25rem;
text-align: center;
text-transform: uppercase;
line-height: @tab-height;
border-top: 0 none;
border-bottom: 0 none;
background-color: @color-tab-bg;
color: @color-tab-text;
gap: 0.25rem;
a.item {
position: relative;
padding: 0 0.5rem;
color: @color-tab-text;
font-family: @font-rivanna;
font-size: @tab-font-size;
text-decoration: none;
line-height: @tab-height;
&:hover {
text-shadow: 1px 0px 0px @color-accent;
}
&.active {
text-shadow: 1px 0px 0px @color-accent;
text-decoration: underline;
}
}
}
/* Force dark text on section titles and labels inside actor sheet body
(overrides Foundry core heading/label colors from the default dark theme) */
section.sheet-body {
h1, h2, h3, h4, label, span, a {
color: @color-text;
}
}
/* ── Belle Époque aesthetic improvements ─────────────────────────── */
@be-bordeaux: #5a0a14;
@be-gold: #8b6914;
@be-gold-border: rgba(139, 105, 20, 0.45);
@be-sepia: #3d2b1f;
// Sheet header — portrait with gold border, charname input styled
.sheet-header {
.profile-img {
border: 2px solid @be-gold-border;
border-radius: 2px;
box-shadow: 1px 1px 4px rgba(0,0,0,0.25);
}
h1.charname input {
font-family: @font-rivanna, serif;
font-size: 1.3rem;
color: @be-bordeaux;
border-bottom: 1px solid @be-gold-border;
border-top: none;
border-left: none;
border-right: none;
background: transparent;
text-shadow: 0 1px 0 rgba(255,255,255,0.4);
}
input[type="text"] {
border-bottom: 1px solid @be-gold-border;
border-top: none;
border-left: none;
border-right: none;
background: transparent;
&:focus {
outline: none;
border-bottom-color: @be-bordeaux;
box-shadow: none;
}
}
}
// Section title rows (Attributs, Armes, Équipements, etc.)
.items-title-bg {
background: linear-gradient(to right, rgba(90,10,20,0.15), rgba(139,105,20,0.10)) !important;
border-bottom: 1px solid @be-gold-border;
border-top: 1px solid @be-gold-border;
margin-top: 6px;
.items-title-text {
font-family: @font-rivanna, serif;
font-size: 0.9rem;
color: @be-bordeaux !important;
letter-spacing: 0.03em;
}
}
// Roll links — attributs and weapons
a.roll-attribut, a.roll-arme {
color: @be-sepia !important;
font-weight: 600;
transition: color 0.12s;
i { color: fade(@be-gold, 80%); margin-left: 0.2rem; font-size: 0.75em; }
&:hover {
color: @be-bordeaux !important;
text-decoration: none;
i { color: @be-bordeaux; }
}
}
// Attribute value inputs
input.item-field-label-short, input.item-field-label-medium {
text-align: center;
font-weight: 700;
font-size: 0.9rem;
color: @be-bordeaux;
border: 1px solid @be-gold-border;
border-radius: 2px;
background: rgba(255,252,240,0.6);
&:focus {
outline: none;
border-color: @be-bordeaux;
box-shadow: 0 0 3px rgba(90,10,20,0.2);
}
}
// Item rows — zebra effect with Belle Époque tones
.alternate-list {
.list-item:nth-child(even) {
background: rgba(139,105,20,0.07);
}
.list-item:nth-child(odd) {
background: rgba(61,43,31,0.04);
}
.list-item:hover {
background: rgba(139,105,20,0.15) !important;
}
}
// Item icon — small gold border
.sheet-competence-img {
border: 1px solid @be-gold-border;
border-radius: 1px;
}
// Item control icons (edit/delete)
.item-controls a.item-control {
color: fade(@be-sepia, 60%);
&:hover { color: @be-bordeaux; }
}
// Biography tab inputs
.tab.biodata {
input[type="text"] {
border-bottom: 1px solid @be-gold-border;
border-top: none;
border-left: none;
border-right: none;
background: transparent;
color: @be-sepia;
&:focus {
outline: none;
border-bottom-color: @be-bordeaux;
}
}
label.item-name-label-medium {
font-weight: 600;
color: @be-sepia !important;
}
}
}

202
less/base.less Normal file
View File

@@ -0,0 +1,202 @@
/* ==================== (A) Fonts ==================== */
@font-face {
font-family: "Rivanna";
src: url('../fonts/rivanna.ttf') format("truetype");
}
:root {
/* =================== 1. ACTOR SHEET FONT STYLES =========== */
--window-header-title-font-size: 1.3rem;
--window-header-title-font-weight: normal;
--window-header-title-color: #f5f5f5;
--major-button-font-size: 1.05rem;
--major-button-font-weight: normal;
--major-button-color: #dadada;
--tab-header-font-size: 1.0rem;
--tab-header-font-weight: 700;
--tab-header-color: #403f3e;
--tab-header-color-active: #4a0404;
--actor-input-font-size: 0.8rem;
--actor-input-font-weight: 500;
--actor-input-color: black;
--actor-label-font-size: 0.8rem;
--actor-label-font-weight: 700;
--actor-label-color: #464331c4;
/* =================== 2. DEBUGGING HIGHLIGHTERS ============ */
--debug-background-color-red: #ff000054;
--debug-background-color-blue: #1d00ff54;
--debug-background-color-green: #54ff0054;
--debug-box-shadow-red: inset 0 0 2px red;
--debug-box-shadow-blue: inset 0 0 2px blue;
--debug-box-shadow-green: inset 0 0 2px green;
}
/*@import url("https://fonts.googleapis.com/css2?family=Martel:wght@400;800&family=Roboto:wght@300;400;500&display=swap");*/
/* Global styles & Font */
.fvtt-malefices .window-app {
text-align: justify;
font-size: 16px;
letter-spacing: 1px;
}
/* Fonts */
.sheet header.sheet-header h1 input, .window-app .window-header, #actors .directory-list, #navigation #scene-list .scene.nav-item {
font-size: 1.0rem;
} /* For title, sidebar character and scene */
.fvtt-malefices .sheet nav.sheet-tabs {
font-size: 0.8rem;
} /* For nav and title */
.window-app input, .fvtt-malefices .item-form, .sheet header.sheet-header .flex-group-center.flex-compteurs, .sheet header.sheet-header .flex-group-center.flex-fatigue, select, button, .item-checkbox, #sidebar, #players, #navigation #nav-toggle {
font-size: 0.8rem;
}
.window-header{
background: rgba(0,0,0,0.75);
}
.window-app.sheet .window-content {
margin: 0;
padding: 0;
}
.strong-text{
font-weight: bold;
}
.tabs .item.active, .blessures-list li ul li:first-child:hover, a:hover {
text-shadow: 1px 0px 0px @color-accent;
}
.rollable:hover, .rollable:focus {
color: #000;
text-shadow: 0 0 10px red;
cursor: pointer;
}
input:hover, select:hover {
border-width: 4px;
border-color: rgb(85, 65, 130);
}
input:disabled {
color:#1c2058;
}
select:disabled {
color:#1c2058;
}
table {border: 1px solid #7a7971;}
.grid, .grid-2col {
display: grid;
grid-column: span 2 / span 2;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
margin: 10px 0;
padding: 0;
}
.grid-3col {
grid-column: span 3 / span 3;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid-4col {
grid-column: span 4 / span 4;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.grid-5col {
grid-column: span 5 / span 5;
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.grid-6col {
grid-column: span 5 / span 5;
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.grid-7col {
grid-column: span 7 / span 7;
grid-template-columns: repeat(7, minmax(0, 1fr));
}
.grid-8col {
grid-column: span 8 / span 8;
grid-template-columns: repeat(8, minmax(0, 1fr));
}
.grid-9col {
grid-column: span 9 / span 9;
grid-template-columns: repeat(9, minmax(0, 1fr));
}
.grid-10col {
grid-column: span 10 / span 10;
grid-template-columns: repeat(10, minmax(0, 1fr));
}
.grid-11col {
grid-column: span 11 / span 11;
grid-template-columns: repeat(11, minmax(0, 1fr));
}
.grid-12col {
grid-column: span 12 / span 12;
grid-template-columns: repeat(12, minmax(0, 1fr));
}
.flex-group-center,
.flex-group-left,
.flex-group-right {
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
text-align: center;
padding: 5px;
}
.flex-group-left {
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
text-align: left;
}
.flex-group-right {
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
text-align: right;
}
.flex-center {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
text-align: center;
}
.table-create-actor {
font-size: 0.8rem;
}
.flex-between {
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
}
.flex-shrink {
flex: 'flex-shrink' ;
}

325
less/chat.less Normal file
View File

@@ -0,0 +1,325 @@
// ============================================================
// Chat messages Belle Époque theme
// ============================================================
@be-bordeaux: #5a0a14;
@be-gold: #8b6914;
@be-gold-light: #c8a84b;
@be-gold-border: rgba(139, 105, 20, 0.45);
@be-sepia: #3d2b1f;
@be-sepia-light: rgba(61, 43, 31, 0.06);
.malefices-chat-card {
font-size: 0.85rem;
color: @be-sepia;
border: 1px solid @be-gold-border;
border-radius: 3px;
overflow: hidden;
background: rgb(218, 218, 208); // opaque pour masquer le fond parchemin du sidebar
// ── Header ──────────────────────────────────────────────
.chat-card-header {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.2rem 0.4rem;
background: linear-gradient(135deg, rgba(90,10,20,0.12), rgba(139,105,20,0.12));
border-bottom: 1px solid @be-gold-border;
.chat-actor-img {
width: 32px;
height: 32px;
border: 1px solid @be-gold-border;
border-radius: 2px;
object-fit: cover;
flex-shrink: 0;
}
.chat-actor-name {
font-family: @font-rivanna, serif;
font-size: 1.1rem;
font-weight: normal;
color: @be-bordeaux;
line-height: 1.1;
text-shadow: 0 1px 0 rgba(255,255,255,0.5);
flex: 1;
}
.chat-header-roll {
display: flex;
align-items: center;
gap: 0.3rem;
padding-left: 0.4rem;
border-left: 1px solid @be-gold-border;
.chat-roll-icon {
width: 24px;
height: 24px;
border: 1px solid @be-gold-border;
border-radius: 2px;
object-fit: cover;
flex-shrink: 0;
}
.chat-roll-name {
font-size: 0.75rem;
color: fade(@be-sepia, 80%);
font-style: italic;
max-width: 80px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
// ── Decorative separator ─────────────────────────────────
.chat-card-separator {
height: 14px;
margin: 0.15rem 0.4rem 0;
background: url("../images/ui/separator_01.webp") center/auto 100% no-repeat;
opacity: 0.55;
}
// ── Roll detail rows ─────────────────────────────────────
.chat-roll-details {
margin: 0;
padding: 0 0.4rem 0.15rem;
.chat-detail-row {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 0.05rem 0;
border-bottom: 1px dotted rgba(139, 105, 20, 0.2);
&:last-child { border-bottom: none; }
dt {
color: fade(@be-sepia, 75%);
font-weight: normal;
font-size: 0.8rem;
}
dd {
font-weight: 600;
color: @be-sepia;
font-size: 0.85rem;
margin-left: 0.5rem;
}
}
// Target and roll value stand out slightly
.chat-detail-target dd,
.chat-detail-roll dd {
font-size: 1rem;
color: @be-bordeaux;
}
}
// ── Result banner ────────────────────────────────────────
.chat-card-result {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.1rem;
margin: 0.2rem 0.4rem 0.25rem;
padding: 0.25rem 0.5rem;
border-radius: 2px;
font-family: @font-rivanna, serif;
font-size: 1.15rem;
text-align: center;
border: 1px solid transparent;
&.result-success {
background: linear-gradient(135deg, rgba(20, 80, 20, 0.12), rgba(40, 100, 40, 0.08));
border-color: rgba(30, 100, 30, 0.35);
color: #1a5c1a;
}
&.result-failure {
background: linear-gradient(135deg, rgba(90, 10, 20, 0.12), rgba(120, 20, 20, 0.08));
border-color: rgba(90, 10, 20, 0.35);
color: @be-bordeaux;
}
i { margin-right: 0.3rem; }
.chat-result-damage {
font-family: sans-serif;
font-size: 0.85rem;
opacity: 0.85;
font-style: normal;
}
}
// ── Action buttons (Relancer, etc.) ──────────────────────
.chat-card-actions {
padding: 0.15rem 0.4rem 0.25rem;
display: flex;
justify-content: center;
.chat-card-button {
padding: 0.3rem 1rem;
font-size: 0.82rem;
border: 1px solid @be-gold-border;
border-radius: 2px;
background: linear-gradient(to bottom, rgba(255,252,240,0.9), rgba(240,230,200,0.9));
color: @be-sepia;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background: linear-gradient(to bottom, rgba(240,220,170,0.95), rgba(220,195,140,0.95));
border-color: @be-gold;
color: @be-bordeaux;
}
}
}
// ── Tarot card display ──────────────────────────────────
.tarot-card-display {
display: flex;
gap: 0.6rem;
align-items: flex-start;
padding: 0.3rem 0.5rem 0.4rem;
position: relative;
&:hover {
z-index: 100;
.tarot-card-img {
transform: scale(2.5);
box-shadow: 3px 3px 12px rgba(0,0,0,0.4);
}
}
.tarot-card-img {
width: 126px;
flex-shrink: 0;
border: 1px solid @be-gold-border;
border-radius: 2px;
box-shadow: 1px 1px 4px rgba(0,0,0,0.2);
transition: transform 0.2s ease, box-shadow 0.2s ease;
transform-origin: left center;
cursor: zoom-in;
}
.tarot-card-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding-top: 0.2rem;
.tarot-card-name {
font-family: @font-rivanna, serif;
font-size: 1rem;
color: @be-bordeaux;
text-shadow: 0 1px 0 rgba(255,255,255,0.4);
}
.tarot-card-side {
font-size: 0.8rem;
font-weight: 600;
&.tarot-positif { color: #1a5c1a; }
&.tarot-negatif { color: @be-bordeaux; }
}
.tarot-card-value {
font-size: 0.82rem;
color: @be-sepia;
strong { color: @be-bordeaux; font-size: 1rem; }
}
}
}
}
// ── Item post message ────────────────────────────────────
.malefices-chat-card.malefices-chat-item {
.item-type-label {
font-family: "Cinzel Decorative", "Cinzel", serif;
font-size: 0.65rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: @be-bordeaux;
opacity: 0.8;
margin-left: auto;
align-self: center;
}
.item-damage {
font-weight: 700;
color: @be-bordeaux;
font-size: 1rem;
&.item-damage-crit {
color: #8b0000;
}
}
.chat-item-description {
margin: 0.4rem 0.5rem 0.3rem;
font-size: 0.82rem;
color: @be-sepia;
line-height: 1.5;
border-top: 1px solid fade(@be-gold-border, 50%);
padding-top: 0.4rem;
p { margin: 0 0 0.3rem; &:last-child { margin: 0; } }
em { color: @be-bordeaux; font-style: italic; }
strong { color: @be-sepia; }
}
}
// ── Welcome message ─────────────────────────────────────
.malefices-chat-card.malefices-welcome {
.welcome-body {
padding: 0.3rem 0.6rem 0.4rem;
font-size: 0.82rem;
color: @be-sepia;
line-height: 1.4;
p { margin: 0.2rem 0; }
.welcome-title {
font-family: @font-rivanna, serif;
font-size: 0.95rem;
color: @be-bordeaux;
font-weight: normal;
margin-bottom: 0.35rem;
}
a { color: @be-gold; &:hover { color: @be-bordeaux; } }
}
.welcome-commands {
margin-top: 0.4rem;
border-top: 1px solid @be-gold-border;
padding-top: 0.3rem;
.welcome-commands-title {
font-weight: 700;
font-size: 0.8rem;
color: @be-sepia;
margin-bottom: 0.2rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.welcome-command-row {
font-size: 0.8rem;
color: @be-sepia;
margin: 0.1rem 0;
code {
background: rgba(139,105,20,0.12);
border: 1px solid @be-gold-border;
border-radius: 2px;
padding: 0 3px;
font-size: 0.78rem;
color: @be-bordeaux;
}
}
}
}

616
less/components.less Normal file
View File

@@ -0,0 +1,616 @@
/* ======================================== */
/* Global UI elements */
/* ======================================== */
h1, h2, h3, h4 {
font-weight: bold;
}
ul, ol {
margin: 0;
padding: 0;
}
ul, li {
list-style-type: none;
}
.sheet li {
margin: 0.010rem;
padding: 0.25rem;
}
.header-fields li {
margin: 0;
padding: 0;
}
.alterne-list > .list-item:hover {
background: rgba(100, 100, 50, 0.25);
}
.alterne-list > .list-item:nth-child(even) {
background: rgba(80, 60, 0, 0.10);
}
.alterne-list > .list-item:nth-child(odd) {
background: rgb(160, 130, 100, 0.05);
}
.specialisation-label {
font-size: 0.8rem;
}
.carac-label,
.attr-label {
font-weight: bold;
}
.list-item {
margin: 0.125rem;
/*box-shadow: inset 0px 0px 1px #00000096;
border-radius: 0.25rem;*/
padding: 0.125rem;
flex: 1 1 5rem;
display: flex !important;
color: @color-text;
}
.list-item-shadow {
background:rgba(87, 60, 32, 0.35);
flex-grow: 0;
flex-wrap: nowrap;
justify-content: flex-start;
}
.list-item-shadow2 {
background:rgba(87, 60, 32, 0.25);
flex-grow: 0;
flex-wrap: nowrap;
justify-content: flex-start;
}
.item-display-show {
display: block;
}
.item-display-hide {
display: none;
}
.item-quantite {
margin-left: 0.5rem;
}
.list-item-margin1 {
margin-left: 1rem;
}
.list-item-margin2 {
margin-left: 2rem;
}
.list-item-margin3 {
margin-left: 3rem;
}
.list-item-margin4 {
margin-left: 4rem;
}
.sheet-competence-img {
width: 24px;
max-width: 24px;
height: 24px;
max-height: 24px;
flex-grow: 0;
margin-right: 0.25rem;
}
.competence-column {
flex-direction: column;
align-content: flex-start;
justify-content: flex-start;
flex-grow: 0;
flex-basis: 1;
}
.competence-header {
align-content: flex-start;
justify-content: flex-start;
font-weight: bold;
flex-grow: 0;
}
.description-label {
flex-grow: 2;
margin-left: 4px;
}
.status-header-label {
margin-left: 2px;
}
.roll-dialog-label {
margin: 4px 0;
min-width: 96px;
}
.short-label {
flex-grow: 1;
}
.keyword-label {
font-size: 0.85rem;
}
.item-sheet-label {
flex-grow: 1;
}
.item-text-long-line {
flex-grow: 3;
}
.score-label {
flex-grow: 2;
align-content: center;
}
.attribut-value,
.carac-value {
flex-grow: 0;
flex-basis: 64px;
margin-right: 4px;
margin-left: 4px;
}
.sante-value,
.competence-value {
flex-grow: 0;
flex-basis: 2rem;
margin-right: 0.25rem;
margin-left: 0.25rem;
}
.description-value {
flex-grow: 0;
flex-basis: 4rem;
margin-right: 0.25rem;
margin-left: 0.25rem;
}
.small-label {
margin-top: 5px;
}
.padd-right {
margin-right: 8px;
}
.padd-left {
margin-left: 8px;
}
.stack-left {
align-items:center;
flex-shrink: 1;
flex-grow: 0;
}
.packed-left {
white-space: nowrap;
flex-grow: 0;
}
.input-numeric-short {
width: 40px;
max-width: 40px;
flex-grow: 0;
flex-shrink: 0;
flex-basis: 40px;
margin-right: 0.25rem;
margin-left: 0.25rem;
}
.abilities-table {
align-content: flex-start;
}
/* ======================================== */
.tokenhudext {
display: flex;
flex: 0 !important;
font-weight: 600;
}
.tokenhudext.left {
justify-content: flex-start;
flex-direction: column;
position: absolute;
top: 2.75rem;
right: 4rem;
}
.tokenhudext.right {
justify-content: flex-start;
flex-direction: column;
position: absolute;
top: 2.75rem;
left: 4rem;
}
.control-icon.tokenhudicon {
width: fit-content;
height: fit-content;
min-width: 6rem;
flex-basis: auto;
padding: 0;
line-height: 1rem;
margin: 0.25rem;
}
.control-icon.tokenhudicon.right {
margin-left: 8px;
}
#token-hud .status-effects.active{
z-index: 2;
}
.token-sheet .window-content .flexcol .sheet-tabs {
font-size: 0.8rem;
}
/* ======================================== */
.item-checkbox {
height: 25px;
border: 1px solid #736953a6;
border-left: none;
font-weight: 500;
font-size: 1rem;
color: black;
padding-top: 5px;
margin-right: 0px;
width: 45px;
position: relative;
left: 0px;
text-align: center;
}
.skill-label {
font-size: 0.7rem;
}
.skill-good-checkbox {
max-height: 10px;
max-width: 10px;
}
.flex-actions-bar {
flex-grow: 2;
}
/* ======================================== */
/* Sidebar CSS */
#sidebar {
font-size: 1rem;
/*background-position: 100%;*/
background-color: @color-sidebar-bg;
background-position: 0px 35px;
background-repeat: no-repeat;
background-image: @bg-sheet;
color: @color-text;
}
#sidebar .scene {
color: rgba(237, 240, 199, 0.95);
}
/* background: rgb(105,85,65) url("../images/ui/texture_feuille_perso_onglets.webp") no-repeat right bottom;*/
#sidebar.collapsed {
height: 470px !important;
}
#sidebar-tabs > .collapsed, #chat-controls .chat-control-icon {
color: @color-text;
text-shadow: 1px 1px 0 rgba(0,0,0,0.75);
}
.sidebar-tab .directory-list .entity {
border-top: 1px dashed rgba(0,0,0,0.25);
border-bottom: 0 none;
padding: 0.25rem 0;
}
.sidebar-tab .directory-list .entity:hover {
background: rgba(0,0,0,0.05);
cursor: pointer;
}
.chat-message-header {
background: rgba(220,220,210,0.5);
font-size: 1.1rem;
height: 48px;
text-align: center;
vertical-align: middle;
display: flex;
align-items: center;
}
.chat-message .message-header .flavor-text, .chat-message .message-header .whisper-to {
font-size: 0.9rem;
}
.chat-result-text,
.chat-actor-name {
font-weight: bold;
font-family: Rivanna;
font-size: 1.2rem;
padding: 4px;
}
.chat-result-success {
color:darkgreen;
}
.chat-result-failure {
color:darkred;
}
.chat-img {
width: 64px;
height: 64px;
}
.roll-dialog-header {
height: 52px;
}
.actor-icon {
float: left;
width: 48px;
height: 48px;
padding: 2px 6px 2px 2px;
}
.padding-dice {
padding-top: .2rem;
padding-bottom: .2rem;
}
.dice-image {
box-sizing: border-box;
border: none;
border-radius: 0;
max-width: 100%;
}
.dice-image-reroll {
background-color:rgba(115, 224, 115, 0.25);
border-color: #011d33;
box-sizing: border-box;
border: 1px;
border-radius: 0%;
max-width: 100%;
}
.chat-dice {
width: 15%;
height: 15%;
font-size: 15px;
padding: 10px;
padding-bottom: 20px;
padding-top: .2rem;
padding-bottom: .2rem;
}
.div-center {
align-self: center;
}
.chat-message {
background: rgba(220,220,210,0.5);
font-size: 0.9rem;
}
.chat-message.whisper {
background: rgba(220,220,210,0.75);
border: 2px solid #545469;
}
.chat-message .chat-icon {
border: 0;
padding: 2px 6px 2px 2px;
float: left;
width: 64px;
height: 64px;
}
.ability-icon {
border: 0;
padding: 2px 2px 2px 2px;
max-width:32px;
max-height:32px;
width: auto;
height: auto;
}
.small-ability-icon {
border: 0;
padding: 2px 2px 2px 2px;
max-width:16px;
max-height:16px;
width: auto;
height: auto;
}
.combat-icon {
border: 0;
padding: 2px 2px 2px 2px;
max-width:24px;
max-height:24px;
width: auto;
height: auto;
}
#sidebar-tabs {
flex: 0 0 32px;
box-sizing: border-box;
margin: 0 0 5px;
border-bottom: 1px solid rgba(0,0,0,0);
box-shadow: inset 0 0 2rem rgba(0,0,0,0.5);
}
#sidebar-tabs > .item.active {
border: 1px solid rgba(114,98,72,1);
background: rgba(30, 25, 20, 0.75);
box-shadow: 0 0 6px inset rgba(114,98,72,1);
}
#sidebar #sidebar-tabs i{
display: inline-block;
background-position:center;
background-size:cover;
text-shadow: 1px 1px 0 rgba(0,0,0,0.75);
}
/*--------------------------------------------------------------------------*/
/* Control, Tool, hotbar & navigation */
#controls .scene-control, #controls .control-tool {
box-shadow: 0 0 3px #000;
margin: 0 0 8px;
border-radius: 0;
background: rgba(30, 25, 20, 1);
background-origin: padding-box;
border-image: url(img/ui/footer-button.png) 10 repeat;
border-image-width: 4px;
border-image-outset: 0px;
}
#controls .scene-control.active, #controls .control-tool.active, #controls .scene-control:hover, #controls .control-tool:hover {
background: rgba(72, 46, 28, 1);
background-origin: padding-box;
border-image: url(img/ui/footer-button.png) 10 repeat;
border-image-width: 4px;
border-image-outset: 0px;
box-shadow: 0 0 3px #ff6400;
}
#hotbar #action-bar #macro-list {
border: 1px solid rgba(72, 46, 28, 1);
box-shadow: 2px 2px 5px #000000;
}
#hotbar #action-bar .macro {
border-image: url(img/ui/bg_control.jpg) 21 repeat;
border-image-slice: 6 6 6 6 fill;
border-image-width: 6px 6px 6px 6px;
border-image-outset: 0px 0px 0px 0px;
border-radius: 0px;
}
#hotbar .bar-controls {
background: rgba(30, 25, 20, 1);
border: 1px solid rgba(72, 46, 28, 1);
}
#players {
border-image: url(img/ui/footer-button.png) 10 repeat;
border-image-width: 4px;
border-image-outset: 0px;
background: rgba(30, 25, 20, 1);
}
#navigation #scene-list .scene.nav-item.active {
background: rgba(72, 46, 28, 1);
}
#navigation #scene-list .scene.nav-item {
background: rgba(30, 25, 20, 1);
background-origin: padding-box;
border-image: url(img/ui/footer-button.png) 10 repeat;
border-image-width: 4px;
border-image-outset: 0px;
}
#navigation #scene-list .scene.view, #navigation #scene-list .scene.context {
background: rgba(72, 46, 28, 1);
background-origin: padding-box;
border-image: url(img/ui/footer-button.png) 10 repeat;
border-image-width: 4px;
border-image-outset: 0px;
box-shadow: 0 0 3px #ff6400;
}
#navigation #nav-toggle {
background: rgba(30, 25, 20, 1);
background-origin: padding-box;
border-image: url(img/ui/footer-button.png) 10 repeat;
border-image-width: 4px;
border-image-outset: 0px;
}
/* Tooltip container */
.tooltip {
position: relative;
display: inline-block;
/*border-bottom: 1px dotted black; /* If you want dots under the hoverable text */
}
/* Tooltip text */
.tooltip .tooltiptext {
text-align: left;
background: rgba(231, 229, 226, 0.9);
width: 150px;
padding: 3px 0;
font-size: 0.9rem;
/* Position the tooltip text */
top: 1px;
position: absolute;
z-index: 1;
/* Fade in tooltip */
visibility: hidden;
opacity: 0;
transition: opacity 0.3s;
}
.tooltip-nobottom {
border-bottom: unset; /* If you want dots under the hoverable text */
}
/* Show the tooltip text when you mouse over the tooltip container */
.tooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
}
.chat-card-button {
box-shadow: inset 0px 1px 0px 0px #a6827e;
background: linear-gradient(to bottom, #21374afc 5%, #152833ab 100%);
background-color: #7d5d3b00;
border-radius: 3px;
border: 2px ridge #846109;
display: inline-block;
cursor: pointer;
color: #ffffff;
font-size: 0.8rem;
padding: 4px 12px 0px 12px;
text-decoration: none;
text-shadow: 0px 1px 0px #4d3534;
position: relative;
margin:2px;
}
.chat-card-button:hover {
background: linear-gradient(to bottom, #800000 5%, #3e0101 100%);
background-color: red;
}
.chat-card-button:active {
position:relative;
top:1px;
}
.plus-minus-button {
box-shadow: inset 0px 1px 0px 0px #a6827e;
background: linear-gradient(to bottom, #21374afc 5%, #152833ab 100%);
background-color: #7d5d3b00;
border-radius: 2px;
border: 1px ridge #846109;
display: inline-block;
cursor: pointer;
color: #ffffff;
margin: 2px 2px 2px 2px;
padding: 2px 2px 2px 2px;
text-decoration: none;
text-shadow: 0px 1px 0px #4d3534;
position: relative;
margin:0px;
}
.plus-minus-button:hover,
.chat-card-button:hover {
background: linear-gradient(to bottom, #800000 5%, #3e0101 100%);
background-color: red;
}
.plus-minus-button:active,
.chat-card-button:active {
position:relative;
top:1px;
}
.plus-minus {
font-size: 0.9rem;
font-weight: bold;
}
.ul-level1 {
padding-left: 2rem;
}

469
less/dialogs.less Normal file
View File

@@ -0,0 +1,469 @@
/* ===================================================================
AppV2 Dialogs — Style Belle Époque (France, ~1900)
Palette : bordeaux, or antique, sépia, fond parchemin clair
=================================================================== */
// Couleurs Belle Époque
@be-bordeaux: #5a0a14;
@be-gold: #8b6914;
@be-gold-light: rgba(139, 105, 20, 0.25);
@be-gold-border: rgba(139, 105, 20, 0.55);
@be-sepia: #3d2b1f;
@be-sepia-light: rgba(61, 43, 31, 0.08);
.malefices-roll-dialog {
.window-content {
padding: 0;
background: @bg-sheet;
color: @be-sepia;
font-size: 0.85rem;
}
.skill-roll-dialog {
display: flex;
flex-direction: column;
// ── En-tête ──────────────────────────────────────────────
header.roll-dialog-header {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.75rem;
padding: 0.6rem 0.75rem 0.5rem;
background: linear-gradient(to bottom, rgba(90, 10, 20, 0.12), rgba(90, 10, 20, 0.04));
border-bottom: 2px solid @be-gold-border;
}
.actor-icon {
width: 52px;
height: 52px;
object-fit: cover;
border: 2px solid @be-gold-border;
border-radius: 2px;
box-shadow: 0 1px 4px rgba(0,0,0,0.35);
flex-shrink: 0;
}
// Custom title div — no h1 to avoid Foundry theme overrides
.dialog-roll-title {
color: @be-bordeaux;
font-family: @font-rivanna;
font-size: 2rem;
font-weight: bold;
flex: 1;
line-height: 1.1;
text-shadow: 1px 1px 2px rgba(255,255,255,0.6);
}
// ── Séparateur décoratif ──────────────────────────────────
.dialog-separator {
width: 100%;
height: 18px;
background: url("../images/ui/separator_01.webp") center/auto 100% no-repeat;
opacity: 0.7;
margin: 0.1rem 0;
}
// ── Corps du dialog ───────────────────────────────────────
.dialog-body {
display: flex;
flex-direction: column;
gap: 0.4rem;
padding: 0.5rem 0.75rem 0.75rem;
}
// Ligne attribut principal
.dialog-attribute-row {
display: flex;
flex-direction: row;
align-items: baseline;
gap: 0.5rem;
padding: 0.3rem 0.5rem;
background: @be-gold-light;
border: 1px solid @be-gold-border;
border-radius: 2px;
.dialog-attr-label {
font-family: @font-rivanna;
font-size: 1.1rem;
color: @be-bordeaux;
font-weight: bold;
flex: 1;
}
.dialog-attr-value {
font-family: @font-rivanna;
font-size: 1.3rem;
font-weight: bold;
color: @be-sepia;
min-width: 2rem;
text-align: right;
}
}
// Titre de section (éléments biographiques)
.dialog-section-title {
font-family: @font-rivanna;
font-size: 0.95rem;
color: @be-gold;
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 1px solid @be-gold-border;
padding-bottom: 0.15rem;
margin-top: 0.25rem;
}
// Liste des éléments biographiques
.dialog-bio-list {
list-style: none;
margin: 0 0 0.25rem 0;
padding: 0 0 0 0.5rem;
li {
color: @be-sepia;
font-size: 0.8rem;
padding: 0.1rem 0;
border-bottom: 1px dashed rgba(139, 105, 20, 0.2);
&:last-child { border-bottom: none; }
&::before {
content: "✦ ";
color: @be-gold;
font-size: 0.65rem;
}
}
}
// Zone des modificateurs
.dialog-modifiers {
display: flex;
flex-direction: column;
gap: 0.3rem;
margin-top: 0.2rem;
}
.dialog-modifier-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0.5rem;
border-bottom: 1px solid rgba(139, 105, 20, 0.18);
&:hover {
background: @be-sepia-light;
}
.dialog-modifier-label {
flex: 1;
color: @be-sepia;
font-size: 0.82rem;
font-weight: 600;
cursor: default;
}
select {
flex: 0 0 140px;
width: 140px;
border: 1px solid @be-gold-border;
background: rgba(255, 252, 245, 0.85);
color: @be-sepia;
font-size: 0.82rem;
padding: 1px 4px;
border-radius: 2px;
&:hover {
border-color: @be-gold;
border-width: 2px;
}
&:focus {
outline: none;
border-color: @be-bordeaux;
box-shadow: 0 0 3px rgba(90, 10, 20, 0.3);
}
}
}
}
footer.form-footer {
display: flex;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: linear-gradient(to bottom, rgba(139, 105, 20, 0.08), rgba(139, 105, 20, 0.16));
border-top: 1px solid @be-gold-border;
button {
flex: 1;
padding: 0.4rem 0.75rem;
font-size: 0.9rem;
font-weight: 600;
border: 1px solid @be-gold-border;
border-radius: 2px;
background: linear-gradient(to bottom, rgba(255, 252, 240, 0.9), rgba(240, 230, 200, 0.9));
color: @be-sepia;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background: linear-gradient(to bottom, rgba(240, 220, 170, 0.95), rgba(220, 195, 140, 0.95));
border-color: @be-gold;
color: @be-bordeaux;
}
&[data-action="roll"],
&[type="submit"] {
background: linear-gradient(to bottom, @be-bordeaux, darken(@be-bordeaux, 8%));
color: rgba(255, 245, 220, 0.95);
border-color: darken(@be-bordeaux, 10%);
font-size: 0.95rem;
&:hover {
background: linear-gradient(to bottom, lighten(@be-bordeaux, 5%), @be-bordeaux);
border-color: @be-bordeaux;
color: #fff;
}
}
}
}
}
/* ===================================================================
Dialogs AppV2 partagés (class: MaleficesDialog)
— utilisé par Tirage Tarot et Résumé des Personnages
=================================================================== */
.MaleficesDialog {
.window-content {
background: @bg-sheet;
padding: 0;
overflow-y: auto;
overflow-x: hidden;
}
// ── Résumé des Personnages ─────────────────────────────
.character-summary-container {
padding: 0.5rem 0.75rem;
color: @be-sepia;
.items-list {
list-style: none;
margin: 0;
padding: 0;
.item {
display: flex;
align-items: center;
padding: 0.2rem 0.4rem;
border-bottom: 1px solid fade(@be-gold-border, 40%);
&:last-child { border-bottom: none; }
&.item-header {
background: linear-gradient(135deg, rgba(90,10,20,0.10), rgba(139,105,20,0.10));
border: 1px solid @be-gold-border;
border-radius: 2px;
margin-bottom: 0.25rem;
font-family: "Cinzel Decorative", "Cinzel", serif;
font-size: 0.65rem;
font-weight: 700;
letter-spacing: 0.04em;
color: @be-bordeaux;
text-transform: uppercase;
&:not(:first-child) { margin-top: 0.5rem; }
}
&:hover:not(.item-header) {
background: rgba(139, 105, 20, 0.08);
}
}
.item-field {
flex: 1;
text-align: center;
font-size: 0.85rem;
&.item-name { flex: 3; text-align: left; font-weight: 600; }
&.item-name-label-long { flex: 3; text-align: left; }
&.item-name-label-short { flex: 1; text-align: center; }
&.right { text-align: right; }
}
a.summary-roll, a.actor-open {
cursor: pointer;
color: @be-sepia;
&:hover { color: @be-bordeaux; text-decoration: underline; }
}
.actor-delete {
color: fade(@be-sepia, 50%);
font-size: 0.75rem;
cursor: pointer;
&:hover { color: @be-bordeaux; }
}
}
}
.tirage-tarot-dialog {
display: block;
padding: 0.6rem 0.75rem;
color: @be-sepia;
// ── Sélection joueur / attribution ────────────────────
.tirage-select-row {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.4rem 0.6rem;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, rgba(90,10,20,0.08), rgba(139,105,20,0.08));
border: 1px solid @be-gold-border;
border-radius: 2px;
.tirage-select-label {
flex: 1;
font-size: 0.85rem;
font-weight: 600;
color: @be-sepia;
}
.tirage-select {
flex: 0 0 180px;
width: 180px;
border: 1px solid @be-gold-border;
background: rgba(255,252,240,0.85);
color: @be-sepia;
font-size: 0.85rem;
padding: 2px 4px;
border-radius: 2px;
&:focus { outline: none; border-color: @be-bordeaux; }
}
}
.tirage-attribute-row {
background: linear-gradient(135deg, rgba(20,80,20,0.08), rgba(30,100,30,0.05));
border-color: rgba(30,100,30,0.35);
}
// ── Section (main joueur / main secrète) ──────────────
.tirage-section {
display: block;
margin-bottom: 0.5rem;
.tirage-section-title {
display: block;
font-family: @font-rivanna, serif;
font-size: 1rem;
color: @be-bordeaux;
border-bottom: 1px solid @be-gold-border;
padding-bottom: 0.2rem;
margin-bottom: 0.4rem;
text-shadow: 0 1px 0 rgba(255,255,255,0.4);
i { margin-right: 0.35rem; font-size: 0.85em; opacity: 0.7; }
.tirage-player-name {
font-size: 0.85rem;
font-style: italic;
color: @be-sepia;
opacity: 0.8;
}
}
.tirage-secret-title { color: fade(@be-sepia, 70%); }
}
// ── Grille de cartes ──────────────────────────────────
.tirage-cards-row {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
margin-bottom: 0.5rem;
}
.tirage-card {
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 0.2rem;
width: 100px;
vertical-align: top;
&:hover {
position: relative;
z-index: 100;
.tirage-card-img {
transform: scale(2.2);
transform-origin: center top;
box-shadow: 3px 3px 12px rgba(0,0,0,0.4);
}
}
.tirage-card-img {
width: 100px;
border: 1px solid @be-gold-border;
border-radius: 2px;
box-shadow: 1px 1px 4px rgba(0,0,0,0.2);
transition: transform 0.2s ease, box-shadow 0.2s ease;
transform-origin: center center;
cursor: zoom-in;
}
.tirage-card-name {
font-family: @font-rivanna, serif;
font-size: 0.75rem;
color: @be-bordeaux;
text-align: center;
line-height: 1.2;
}
.tirage-card-side {
font-size: 0.7rem;
font-weight: 600;
&.tirage-positif { color: #1a5c1a; }
&.tirage-negatif { color: @be-bordeaux; }
}
&.tirage-card-secret {
opacity: 0.85;
.tirage-card-name { color: fade(@be-sepia, 70%); }
}
}
// ── Séparateur décoratif ──────────────────────────────
.tirage-separator {
display: block;
height: 18px;
background: url("../images/ui/separator_01.webp") center/auto 100% no-repeat;
opacity: 0.5;
margin: 0.25rem 0;
}
}
// ── Footer avec bouton Fermer ─────────────────────────
.tirage-footer {
display: flex;
justify-content: flex-end;
padding: 0.4rem 0.75rem;
border-top: 1px solid @be-gold-border;
background: linear-gradient(to bottom, rgba(139,105,20,0.08), rgba(139,105,20,0.16));
.tirage-close-btn {
padding: 0.3rem 1rem;
font-size: 0.85rem;
border: 1px solid @be-gold-border;
border-radius: 2px;
background: linear-gradient(to bottom, rgba(255,252,240,0.9), rgba(240,230,200,0.9));
color: @be-sepia;
cursor: pointer;
&:hover {
background: linear-gradient(to bottom, rgba(240,220,170,0.95), rgba(220,195,140,0.95));
border-color: @be-gold;
color: @be-bordeaux;
}
}
}
}

136
less/item-sheet.less Normal file
View File

@@ -0,0 +1,136 @@
/* ===================================================================
AppV2 Item Sheets
Selectors use .fvtt-malefices.item (both classes on the same root
element, no space) to target only AppV2 item sheet windows.
=================================================================== */
.fvtt-malefices.item {
/* window-content: background and remove padding.
Foundry AppV2 already sets: display:flex flex-flow:column overflow:hidden */
.window-content {
padding: 0;
background: @bg-sheet;
color: @color-text;
font-size: 0.8rem;
}
/* All sections inside item fill their flex container */
section {
height: 100%;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
}
/* Fixed header bar */
.header {
flex: 0 0 auto;
border-bottom: 1px solid #999;
}
/* sheet-body: scroll container */
section.sheet-body {
overflow-y: auto;
overflow-x: hidden;
padding: 0.25rem 0.5rem;
}
/* Override legacy height: 100% on tab divs; hide all tabs, show only the active one */
.tab[data-tab] {
height: auto;
display: none;
&.active {
display: block;
}
}
/* Header layout override for item sheets (smaller than actor sheet header) */
.sheet-header {
flex: 0 0 auto;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
margin-bottom: 0;
}
.item-sheet-img {
flex: 0 0 64px;
width: 64px;
height: 64px;
object-fit: cover;
border: 1px solid #999;
cursor: pointer;
}
.item-sheet-title {
flex: 1;
h1 {
margin: 0;
padding: 0;
border-bottom: none;
input {
background: transparent;
border: none;
font-family: @font-rivanna;
font-size: 1.8rem;
width: 100%;
height: auto;
}
}
}
.header-actions {
flex: 0 0 auto;
display: flex;
gap: 0.25rem;
align-items: center;
}
/* Tab navigation: AppV2 uses nav.tabs (not nav.sheet-tabs) */
nav.tabs {
display: flex;
flex: 0 0 auto;
font-family: @font-rivanna;
font-size: @tab-font-size;
font-weight: bold;
height: @tab-height;
margin: 0;
padding: 0 0 0 0.25rem;
text-align: center;
text-transform: uppercase;
line-height: @tab-height;
border-top: 0 none;
border-bottom: 0 none;
background-color: @color-tab-bg;
color: @color-tab-text;
gap: 0.25rem;
a.item {
position: relative;
padding: 0 0.5rem;
color: @color-tab-text;
font-family: @font-rivanna;
font-size: @tab-font-size;
text-decoration: none;
line-height: @tab-height;
&:hover {
text-shadow: 1px 0px 0px @color-accent;
}
&.active {
text-shadow: 1px 0px 0px @color-accent;
text-decoration: underline;
}
}
}
}

340
less/legacy-sheets.less Normal file
View File

@@ -0,0 +1,340 @@
/* Styles limited to sheets */
.fvtt-malefices .sheet-header {
-webkit-box-flex: 0;
-ms-flex: 0 0 210px;
flex: 0 0 210px;
overflow: hidden;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
margin-bottom: 10px;
}
.fvtt-malefices .sheet-header .profile-img {
-webkit-box-flex: 0;
-ms-flex: 0 0 128px;
flex: 0 0 128px;
width: 128px;
height: auto;
max-height:128px;
margin-top: 0px;
margin-right: 10px;
object-fit: cover;
object-position: 50% 0;
border-width: 0px;
}
.profile-img-container {
margin-right: 0.2rem;
max-width: 140px;
width: 140px;
}
.button-img {
vertical-align: baseline;
width: 8%;
height: 8%;
max-height: 48px;
border-width: 0px;
border: 1px solid rgba(0, 0, 0, 0);
}
.button-img:hover {
color: rgba(255, 255, 128, 0.7);
border: 1px solid rgba(255, 128, 0, 0.8);
cursor: pointer;
}
.button-effect-img {
vertical-align: baseline;
width: 16px;
max-height: 16px;
height: 16;
border-width: 0;
}
.small-button-container {
height: 16px;
width: 16px;
border: 0;
vertical-align: bottom;
}
.fvtt-malefices .sheet-header .header-fields {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
}
.fvtt-malefices .sheet-header h1.charname {
height: 50px;
padding: 0px;
margin: 5px 0;
border-bottom: 0;
}
.fvtt-malefices .sheet-header h1.charname input {
font-family: Rivanna;
font-size: 3rem;
width: 100%;
height: 100%;
margin: 0;
}
.fvtt-malefices .sheet-tabs {
-webkit-box-flex: 0;
-ms-flex: 0;
flex: 0;
font-family: Rivanna;
font-size: 2.2rem;
}
.fvtt-malefices .sheet-body,
.fvtt-malefices .sheet-body .tab,
.fvtt-malefices .sheet-body .tab .editor {
height: 100%;
font-size: 0.8rem;
}
.editor {
border: 2;
height: 100%;
padding: 0 3px;
}
.medium-editor {
border: 2;
height: 240px;
padding: 0 3px;
}
.small-editor {
border: 2;
height: 120px;
padding: 0 3px;
}
.fvtt-malefices .tox .tox-editor-container {
background: #fff;
}
.fvtt-malefices .tox .tox-edit-area {
padding: 0 8px;
}
.fvtt-malefices .resource-label {
font-weight: bold;
text-transform: uppercase;
}
.fvtt-malefices .tabs {
height: 40px;
border-top: 1px solid #AAA;
border-bottom: 1px solid #AAA;
color: #000000;
}
.fvtt-malefices .tabs .item {
line-height: 40px;
font-weight: bold;
}
.fvtt-malefices .tabs .item.active {
text-decoration: underline;
text-shadow: none;
}
.fvtt-malefices .items-list {
list-style: none;
margin: 1px 0;
padding: 0;
overflow-y: auto;
}
.fvtt-malefices .items-list .item-header {
font-weight: bold;
}
.fvtt-malefices .items-list .item {
height: 30px;
line-height: 24px;
padding: 1px 0;
border-bottom: 1px solid #BBB;
}
.fvtt-malefices .items-list .item .item-image {
-webkit-box-flex: 0;
-ms-flex: 0 0 24px;
flex: 0 0 24px;
margin-right: 5px;
}
.fvtt-malefices .items-list .item img {
display: block;
}
.fvtt-malefices .items-list .item-name {
margin: 0;
}
.fvtt-malefices .items-list .item-controls {
-webkit-box-flex: 0;
-ms-flex: 0 0 86px;
flex: 0 0 86px;
text-align: right;
}
li.folder > .folder-header h3 {
color: @color-text;
}
/* ======================================== */
/* Sheet */
.fvtt-malefices .window-app.sheet .window-content .sheet-header{
color: @color-text;
background: @bg-sheet;
/*background: #494e6b;*/
}
input[type="text"], select[type="text"] {
background:white;
color: @color-input-text;
}
select {
background:white;
color: @color-input-text;
}
/* background: #011d33 url("../images/ui/fond1.webp") repeat left top;*/
/*color: rgba(168, 139, 139, 0.5);*/
.window-app.sheet .window-content .sheet-header select[type="text"], .window-app.sheet .window-content .sheet-header input[type="text"], .window-app.sheet .window-content .sheet-header input[type="number"], .window-app.sheet .window-content .sheet-body input[type="text"], .window-app.sheet .window-content .sheet-body input[type="number"], .window-app.sheet .window-content .sheet-body select[type="text"] {
color: @color-text;
/*color: #494e6b;*/
}
.window-app.sheet .window-content .sheet-header input[type="password"], .window-app.sheet .window-content .sheet-header input[type="date"], .window-app.sheet .window-content .sheet-header input[type="time"] {
color: @color-text;
background: @bg-sheet;
border: 1 none;
margin-bottom: 0.25rem;
margin-left: 2px;
}
.window-app.sheet .window-content .sheet-body input[type="password"], .window-app.sheet .window-content .sheet-body input[type="date"], .window-app.sheet .window-content .sheet-body input[type="time"] {
color: @color-text;
background: @bg-sheet;
border: 1 none;
margin-bottom: 0.25rem;
margin-left: 2px;
}
.window-app.sheet .window-content .sheet-body select, .window-app.sheet .window-content .sheet-header select {
color: @color-text;
background: #fff;
border: 1 none;
margin-bottom: 0.25rem;
margin-left: 2px;
}
.fvtt-malefices .window-app .window-content, .fvtt-malefices .window-app.sheet .window-content .sheet-body{
font-size: 0.8rem;
/*background: url("../images/ui/pc_sheet_bg.webp") repeat left top;*/
background: @bg-sheet;
color: @color-text;
}
/* background: rgba(245,245,240,0.6) url("../images/ui/sheet_background.webp") left top;*/
section.sheet-body{padding: 0.25rem 0.5rem;}
.sheet header.sheet-header .profile-img {
object-fit: cover;
object-position: 50% 0;
margin: 0.5rem 0 0.5rem 0.5rem;
padding: 0;
}
nav.sheet-tabs {
font-size: @tab-font-size;
font-weight: bold;
height: @tab-height;
flex: 0 0 @tab-height;
margin: 0;
padding: 0 0 0 0.25rem;
text-align: center;
text-transform: uppercase;
line-height: 1.5rem;
border-top: 0 none;
border-bottom: 0 none;
background-color: @color-tab-bg;
color: @color-tab-text;
}
/* Dice tray specific overrides */
.dice-tray button svg * {
fill: #6d5923 !important;
}
.dice-tray input[type="text"] {
color: #6d5923 !important;
}
.dice-tray button {
color: #6d5923 !important;
}
nav.sheet-tabs .item {
position: relative;
padding: 0 0.25rem;
color: @color-tab-text;
margin-top: 4px;
margin-bottom: 4px;
}
nav.sheet-tabs .item:after {
content: "";
position: absolute;
top: 0;
right: 0;
height: 2rem;
width: 1px;
/*border-right: 1px dashed rgba(52, 52, 52, 0.25);*/
}
.sheet .tab[data-tab] {
padding: 0;
}
section.sheet-body:after {
content: "";
display: block;
clear: both;
}
.sheet header.sheet-header .flex-compteurs {text-align: right;}
.sheet header.sheet-header .resource-content {width: 2rem;}
.select-diff {
display: inline-block;
text-align: left;
width: 50px;
}
.window-app.sheet .window-content .tooltip:hover .tooltiptext {
top: 2rem;
left: 2rem;
margin: 0;
padding: 0.25rem;
}
.window-app.sheet .window-content .carac-value, .window-app.sheet .window-content .competence-xp {
margin: 0.05rem;
flex-basis: 3rem;
text-align: center;
}

12
less/malefices.less Normal file
View File

@@ -0,0 +1,12 @@
// Main LESS file for Maléfices system
// Importing base styles and component-specific styles
@import "variables";
@import "base";
@import "legacy-sheets";
@import "components";
@import "ui";
@import "dialogs";
@import "chat";
@import "item-sheet";
@import "actor-sheet";

180
less/ui.less Normal file
View File

@@ -0,0 +1,180 @@
/*************************************************************/
#pause
{
font-size: 2rem;
}
#pause > h3
{
color: #CCC
}
#pause > img {
content: url(../images/ui/logo_pause.webp);
height: 200px;
width: 200px;
top: -100px;
left: calc(50% - 132px);
}
#logo {
content : url(../images/ui/logo_pause.webp);
width: 100px;
height: 60px;
}
.dice-cell {
padding-left: 12px;
padding-right: 12px;
width: 60px;
text-align: center;
}
.dice-formula,
.dice-total {
height: 54px;
position:relative;
}
.status-small-label {
font-size: 0.65rem;
}
.no-grow {
flex-grow: 1;
max-width: 32px;
}
.status-col-name {
max-width: 72px;
}
.img-no-border {
max-width: 48px;
max-height: 48px;
border: 0px;
}
.items-title-bg {
margin-top: 6px;
color: @color-text;
}
.items-title-text {
margin-left: 4px;
}
.lock-icon {
width:16px;
height: 16px;
}
.item-sheet-img {
width: 64px;
height: auto;
border: 0;
}
.item-name-img {
flex-grow:1;
max-width: 2rem;
min-width: 2rem;
}
.item-name-label-header {
flex-grow:2;
max-width: 12rem;
min-width: 12rem;
}
.item-name-label-header-long {
flex-grow:2;
max-width: 14rem;
min-width: 14rem;
}
.item-name-label-header-long2 {
flex-grow:2;
max-width: 24rem;
min-width: 24rem;
}
.item-name-label {
flex-grow:2;
max-width: 10rem;
min-width: 10rem;
}
.item-name-label-long {
margin-top: 4px;
flex-grow:2;
max-width: 10rem;
min-width: 10rem;
}
.item-name-label-short {
flex-grow:1;
max-width: 4rem;
min-width: 4rem;
}
.item-name-label-medium {
margin-top: 4px;
flex-grow:2;
max-width: 6rem;
min-width: 6rem;
}
.item-name-label-long2 {
margin-top: 4px;
flex-grow:2;
max-width: 22rem;
min-width: 22rem;
}
.item-name-label-level2 {
flex-grow:2;
max-width: 9rem;
min-width: 9rem;
}
.item-field-label-short {
flex-grow:1;
max-width: 4rem;
min-width: 4rem;
}
.item-field-label-medium {
flex-grow:1;
max-width: 6rem;
min-width: 6rem;
}
.item-field-skill {
flex-grow:1;
max-width: 6.8rem;
min-width: 6.8rem;
}
.item-field-label-long {
flex-grow:1;
max-width: 10rem;
min-width: 10rem;
}
.item-control-end {
align-self: flex-end;
}
.alternate-list {
margin-top: 4px;
flex-wrap: nowrap;
}
.item-filler {
flex-grow: 6;
flex-shrink: 7;
}
.item-controls-fixed {
min-width:2rem;
max-width: 2rem;
}
.attribute-label {
font-weight: bold;
}
.flexrow-no-expand {
flex-grow: 0;
}
.item-input-small {
max-width: 16px;
max-height: 12px;
}
.flip-tarot {
transform: scaleY(-1);
}
.tarot-fixed-width {
width: 140px;
max-width: 140px;
}
.tarot-title {
text-align: center;
font-weight: bold;
}
.character-summary-rollable {
text-decoration: underline;
}

19
less/variables.less Normal file
View File

@@ -0,0 +1,19 @@
// ==================== Variables ====================
// Fonts
@font-rivanna: Rivanna, serif;
// Colors
@color-text: rgba(19, 18, 18, 0.95);
@color-accent: #ff6600;
@color-tab-bg: #252525;
@color-tab-text: beige;
@color-input-text: #494e6b;
@color-sidebar-bg: #f5f5f5;
// Backgrounds
@bg-sheet: url("../images/ui/background_01_clear.webp");
// Tabs
@tab-height: 3rem;
@tab-font-size: 1.2rem;

View File

@@ -0,0 +1,13 @@
/**
* Index des fiches AppV2 pour Maléfices
*/
// Actor sheets
export { default as MaleficesPersonnageSheet } from './malefices-personnage-sheet.mjs';
export { default as MaleficesNPCActorSheet } from './malefices-npc-actor-sheet.mjs';
// Item sheets
export { default as MaleficesArmeSheet } from './malefices-arme-sheet.mjs';
export { default as MaleficesEquipementSheet } from './malefices-equipement-sheet.mjs';
export { default as MaleficesArchetypeSheet } from './malefices-archetype-sheet.mjs';
export { default as MaleficesTarotSheet } from './malefices-tarot-sheet.mjs';
export { default as MaleficesSortilegeSheet } from './malefices-sortilege-sheet.mjs';
export { default as MaleficesElementbioSheet } from './malefices-elementbio-sheet.mjs';

View File

@@ -0,0 +1,132 @@
const { HandlebarsApplicationMixin } = foundry.applications.api
export default class MaleficesItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) {
constructor(options = {}) {
super(options)
this.#dragDrop = this.#createDragDropHandlers()
}
#dragDrop
/** @override */
static DEFAULT_OPTIONS = {
classes: ["fvtt-malefices", "item"],
position: {
width: 620,
height: 600,
},
form: {
submitOnChange: true,
},
window: {
resizable: true,
},
dragDrop: [{ dragSelector: "[data-drag]", dropSelector: null }],
actions: {
editImage: MaleficesItemSheet.#onEditImage,
postItem: MaleficesItemSheet.#onPostItem,
},
}
/** @type {object} */
tabGroups = { primary: "description" }
/** @override */
async _prepareContext() {
const context = {
fields: this.document.schema.fields,
systemFields: this.document.system.schema.fields,
item: this.document,
system: this.document.system,
source: this.document.toObject(),
enrichedDescription: await foundry.applications.ux.TextEditor.implementation.enrichHTML(
this.document.system.description ?? "", { async: true }
),
isEditable: this.isEditable,
cssClass: this.isEditable ? "editable" : "locked",
isGM: game.user.isGM,
config: game.system.malefices.config,
}
return context
}
/** @override */
_onRender(context, options) {
super._onRender(context, options)
this.#dragDrop.forEach((d) => d.bind(this.element))
// Manual tab navigation
const nav = this.element.querySelector('nav.tabs[data-group]')
if (nav) {
const group = nav.dataset.group
const activeTab = this.tabGroups[group] || "description"
nav.querySelectorAll('[data-tab]').forEach(link => {
const tab = link.dataset.tab
link.classList.toggle('active', tab === activeTab)
link.addEventListener('click', (event) => {
event.preventDefault()
this.tabGroups[group] = tab
this.render()
})
})
this.element.querySelectorAll('[data-group="' + group + '"][data-tab]').forEach(content => {
content.classList.toggle('active', content.dataset.tab === activeTab)
})
}
}
// #region Drag-and-Drop
#createDragDropHandlers() {
return this.options.dragDrop.map((d) => {
d.permissions = {
dragstart: this._canDragStart.bind(this),
drop: this._canDragDrop.bind(this),
}
d.callbacks = {
dragstart: this._onDragStart.bind(this),
dragover: this._onDragOver.bind(this),
drop: this._onDrop.bind(this),
}
return new foundry.applications.ux.DragDrop.implementation(d)
})
}
_canDragStart(selector) { return this.isEditable }
_canDragDrop(selector) { return this.isEditable }
_onDragStart(event) {
const dragData = { type: "Item", uuid: this.document.uuid }
event.dataTransfer.setData("text/plain", JSON.stringify(dragData))
}
_onDragOver(event) {}
async _onDrop(event) {}
// #endregion
// #region Actions
static async #onEditImage(event, target) {
const fp = new FilePicker({
type: "image",
current: this.document.img,
callback: (path) => { this.document.update({ img: path }) },
})
return fp.browse()
}
static async #onPostItem(event, target) {
let chatData = foundry.utils.duplicate(this.document)
if (this.document.actor) {
chatData.actor = { id: this.document.actor.id }
}
if (chatData.img?.includes("/blank.png")) {
chatData.img = null
}
chatData.jsondata = JSON.stringify({ compendium: "postedItem", payload: chatData })
const html = await foundry.applications.handlebars.renderTemplate(
'systems/fvtt-malefices/templates/post-item.hbs', chatData
)
ChatMessage.create({ user: game.user.id, content: html })
}
// #endregion
}

View File

@@ -0,0 +1,35 @@
import MaleficesItemSheet from "./base-item-sheet.mjs"
import { MaleficesUtility } from "../../malefices-utility.js"
export default class MaleficesArchetypeSheet extends MaleficesItemSheet {
static DEFAULT_OPTIONS = {
classes: ["archetype"],
position: { width: 620 },
window: { contentClasses: ["archetype-content"] },
}
static PARTS = {
main: { template: "systems/fvtt-malefices/templates/items/item-archetype-sheet.hbs" },
}
tabGroups = { primary: "details" }
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" },
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
context.tarots = MaleficesUtility.getTarots()
return context
}
}

View File

@@ -0,0 +1,33 @@
import MaleficesItemSheet from "./base-item-sheet.mjs"
export default class MaleficesArmeSheet extends MaleficesItemSheet {
static DEFAULT_OPTIONS = {
classes: ["arme"],
position: { width: 640 },
window: { contentClasses: ["arme-content"] },
}
static PARTS = {
main: { template: "systems/fvtt-malefices/templates/items/item-arme-sheet.hbs" },
}
tabGroups = { primary: "details" }
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" },
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
}

View File

@@ -0,0 +1,225 @@
const { HandlebarsApplicationMixin } = foundry.applications.api
import { MaleficesUtility } from "../../malefices-utility.js"
export default class MaleficesActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) {
constructor(options = {}) {
super(options)
this.#dragDrop = this.#createDragDropHandlers()
this._editScore = true
}
#dragDrop
/** @override */
static DEFAULT_OPTIONS = {
classes: ["fvtt-malefices", "actor"],
position: {
width: 640,
height: 680,
},
form: {
submitOnChange: true,
},
window: {
resizable: true,
},
dragDrop: [{ dragSelector: ".item-list .item", dropSelector: null }],
actions: {
editImage: MaleficesActorSheet.#onEditImage,
toggleSheet: MaleficesActorSheet.#onToggleSheet,
editItem: MaleficesActorSheet.#onEditItem,
deleteItem: MaleficesActorSheet.#onDeleteItem,
createItem: MaleficesActorSheet.#onCreateItem,
equipItem: MaleficesActorSheet.#onEquipItem,
modifyQuantity: MaleficesActorSheet.#onModifyQuantity,
modifyAmmo: MaleficesActorSheet.#onModifyAmmo,
rollAttribut: MaleficesActorSheet.#onRollAttribut,
rollArme: MaleficesActorSheet.#onRollArme,
editSubActor: MaleficesActorSheet.#onEditSubActor,
deleteSubActor: MaleficesActorSheet.#onDeleteSubActor,
},
}
/** @type {object} */
tabGroups = { primary: "main" }
/** @override */
async _prepareContext() {
const actor = this.document
return {
actor,
system: actor.system,
source: actor.toObject(),
fields: actor.schema.fields,
systemFields: actor.system.schema.fields,
isEditable: this.isEditable,
cssClass: this.isEditable ? "editable" : "locked",
isGM: game.user.isGM,
config: game.system.malefices.config,
editScore: this._editScore,
}
}
/** @override */
_onRender(context, options) {
super._onRender(context, options)
this.#dragDrop.forEach((d) => d.bind(this.element))
// Ignore Enter key in text inputs (not textarea)
this.element.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA') e.preventDefault()
})
// Manual tab navigation
const nav = this.element.querySelector('nav.tabs[data-group]')
if (nav) {
const group = nav.dataset.group
const activeTab = this.tabGroups[group] || "main"
nav.querySelectorAll('[data-tab]').forEach(link => {
link.classList.toggle('active', link.dataset.tab === activeTab)
link.addEventListener('click', (event) => {
event.preventDefault()
this.tabGroups[group] = link.dataset.tab
this.render()
})
})
this.element.querySelectorAll(`[data-group="${group}"][data-tab]`).forEach(content => {
content.classList.toggle('active', content.dataset.tab === activeTab)
})
}
// Handle .update-field change events (legacy support)
this.element.querySelectorAll('.update-field').forEach(el => {
el.addEventListener('change', (ev) => {
const fieldName = ev.currentTarget.dataset.fieldName
const value = Number(ev.currentTarget.value)
this.actor.update({ [fieldName]: value })
})
})
}
// #region Drag-and-Drop
#createDragDropHandlers() {
return this.options.dragDrop.map((d) => {
d.permissions = {
dragstart: this._canDragStart.bind(this),
drop: this._canDragDrop.bind(this),
}
d.callbacks = {
dragstart: this._onDragStart.bind(this),
dragover: this._onDragOver.bind(this),
drop: this._onDrop.bind(this),
}
return new foundry.applications.ux.DragDrop.implementation(d)
})
}
_canDragStart(selector) { return this.isEditable }
_canDragDrop(selector) { return this.isEditable }
_onDragStart(event) {
const li = event.currentTarget.closest('.item')
if (!li) return
const itemId = li.dataset.itemId
const item = this.actor.items.get(itemId)
if (item) {
event.dataTransfer.setData("text/plain", JSON.stringify({ type: "Item", uuid: item.uuid }))
}
}
_onDragOver(event) {}
async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event)
if (data?.type === "Actor") {
const actor = await fromUuid(data.uuid)
if (actor) this.actor.addSubActor(actor.id)
} else {
super._onDrop(event)
}
}
// #endregion
// #region Actions
static async #onEditImage(event, target) {
const fp = new FilePicker({
type: "image",
current: this.document.img,
callback: (path) => { this.document.update({ img: path }) },
})
return fp.browse()
}
static async #onToggleSheet(event, target) {
this._editScore = !this._editScore
this.render()
}
static async #onEditItem(event, target) {
const li = target.closest(".item")
const itemId = li?.dataset.itemId
if (!itemId) return
this.actor.items.get(itemId)?.sheet.render(true)
}
static async #onDeleteItem(event, target) {
const li = target.closest(".item")
MaleficesUtility.confirmDelete(this, li)
}
static async #onCreateItem(event, target) {
const dataType = target.dataset.type
this.actor.createEmbeddedDocuments('Item', [{ name: "NewItem", type: dataType }], { renderSheet: true })
}
static async #onEquipItem(event, target) {
const li = target.closest(".item")
const itemId = li?.dataset.itemId
if (!itemId) return
await this.actor.equipItem(itemId)
this.render()
}
static async #onModifyQuantity(event, target) {
const li = target.closest(".item")
const itemId = li?.dataset.itemId
if (!itemId) return
const delta = parseInt(target.dataset.delta) || 0
this.actor.incDecQuantity(itemId, delta)
}
static async #onModifyAmmo(event, target) {
const li = target.closest(".item")
const itemId = li?.dataset.itemId
if (!itemId) return
const delta = parseInt(target.dataset.delta) || 0
this.actor.incDecAmmo(itemId, delta)
}
static async #onRollAttribut(event, target) {
const attrKey = target.dataset.attrKey
this.actor.rollAttribut(attrKey)
}
static async #onRollArme(event, target) {
const armeId = target.dataset.armeId
this.actor.rollArme(armeId)
}
static async #onEditSubActor(event, target) {
const li = target.closest(".item")
const actorId = li?.dataset.actorId
if (!actorId) return
game.actors.get(actorId)?.sheet.render(true)
}
static async #onDeleteSubActor(event, target) {
const li = target.closest(".item")
const actorId = li?.dataset.actorId
if (!actorId) return
this.actor.delSubActor(actorId)
}
// #endregion
}

View File

@@ -0,0 +1,32 @@
import MaleficesItemSheet from "./base-item-sheet.mjs"
export default class MaleficesElementbioSheet extends MaleficesItemSheet {
static DEFAULT_OPTIONS = {
classes: ["elementbio"],
position: { width: 620 },
window: { contentClasses: ["elementbio-content"] },
}
static PARTS = {
main: { template: "systems/fvtt-malefices/templates/items/item-elementbio-sheet.hbs" },
}
tabGroups = { primary: "description" }
#getTabs() {
const tabs = {
description: { id: "description", group: "primary", label: "Description" },
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
}

View File

@@ -0,0 +1,32 @@
import MaleficesItemSheet from "./base-item-sheet.mjs"
export default class MaleficesEquipementSheet extends MaleficesItemSheet {
static DEFAULT_OPTIONS = {
classes: ["equipement"],
position: { width: 620 },
window: { contentClasses: ["equipement-content"] },
}
static PARTS = {
main: { template: "systems/fvtt-malefices/templates/items/item-equipement-sheet.hbs" },
}
tabGroups = { primary: "description" }
#getTabs() {
const tabs = {
description: { id: "description", group: "primary", label: "Description" },
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
}

View File

@@ -0,0 +1,27 @@
import MaleficesActorSheet from "./malefices-base-actor-sheet.mjs"
export default class MaleficesNPCActorSheet extends MaleficesActorSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["pnj"],
position: { width: 560, height: 460 },
}
/** @override */
static PARTS = {
main: { template: "systems/fvtt-malefices/templates/actors/npc-sheet.hbs" },
}
/** @override */
tabGroups = { primary: "main" }
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
this.document.system.description ?? "", { async: true }
)
return context
}
}

View File

@@ -0,0 +1,48 @@
import MaleficesActorSheet from "./malefices-base-actor-sheet.mjs"
export default class MaleficesPersonnageSheet extends MaleficesActorSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["personnage"],
position: { width: 640, height: 680 },
}
/** @override */
static PARTS = {
main: { template: "systems/fvtt-malefices/templates/actors/actor-sheet.hbs" },
}
/** @override */
tabGroups = { primary: "main" }
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
const actor = this.document
context.armes = foundry.utils.duplicate(actor.getArmes())
context.tarots = foundry.utils.duplicate(actor.getTarots())
context.tarotsCache = foundry.utils.duplicate(actor.getHiddenTarots())
context.archetype = foundry.utils.duplicate(actor.getArchetype())
context.equipements = foundry.utils.duplicate(actor.getEquipements())
context.elementsbio = actor.getElementsBio()
context.sorts = actor.getSorts()
context.phyMalus = actor.getPhysiqueMalus()
context.subActors = foundry.utils.duplicate(actor.getSubActors())
// Expose nested biodata schema fields for {{formInput}} helper
context.biodataFields = actor.system.schema.fields.biodata.fields
context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
actor.system.biodata?.description ?? "", { async: true }
)
context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
actor.system.biodata?.notes ?? "", { async: true }
)
context.enrichedEquipementlibre = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
actor.system.equipementlibre ?? "", { async: true }
)
return context
}
}

View File

@@ -0,0 +1,33 @@
import MaleficesItemSheet from "./base-item-sheet.mjs"
export default class MaleficesSortilegeSheet extends MaleficesItemSheet {
static DEFAULT_OPTIONS = {
classes: ["sortilege"],
position: { width: 620 },
window: { contentClasses: ["sortilege-content"] },
}
static PARTS = {
main: { template: "systems/fvtt-malefices/templates/items/item-sortilege-sheet.hbs" },
}
tabGroups = { primary: "details" }
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" },
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
}

View File

@@ -0,0 +1,33 @@
import MaleficesItemSheet from "./base-item-sheet.mjs"
export default class MaleficesTarotSheet extends MaleficesItemSheet {
static DEFAULT_OPTIONS = {
classes: ["tarot"],
position: { width: 660, height: 640 },
window: { contentClasses: ["tarot-content"] },
}
static PARTS = {
main: { template: "systems/fvtt-malefices/templates/items/item-tarot-sheet.hbs" },
}
tabGroups = { primary: "details" }
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" },
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
}

View File

@@ -6,16 +6,16 @@
import { MaleficesUtility } from "./malefices-utility.js";
/* -------------------------------------------- */
export class MaleficesActorSheet extends ActorSheet {
export class MaleficesActorSheet extends foundry.appv1.sheets.ActorSheet {
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["fvtt-malefices", "sheet", "actor"],
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["fvtt-malefices", "sheet", "actor", "malefices-actor-sheet"],
template: "systems/fvtt-malefices/templates/actors/actor-sheet.hbs",
width: 640,
height: 640,
height:680,
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "skills" }],
dragDrop: [{ dragSelector: ".item-list .item", dropSelector: null }],
editScore: true
@@ -33,14 +33,20 @@ export class MaleficesActorSheet extends ActorSheet {
name: this.actor.name,
editable: this.isEditable,
cssClass: this.isEditable ? "editable" : "locked",
system: duplicate(this.object.system),
system: foundry.utils.duplicate(this.object.system),
limited: this.object.limited,
armes: duplicate(this.actor.getArmes()),
tarots: duplicate(this.actor.getTarots()),
archetype: duplicate(this.actor.getArchetype()),
equipements: duplicate(this.actor.getEquipements()),
subActors: duplicate(this.actor.getSubActors()),
armes: foundry.utils.duplicate(this.actor.getArmes()),
tarots: foundry.utils.duplicate(this.actor.getTarots()),
tarotsCache: foundry.utils.duplicate(this.actor.getHiddenTarots()),
archetype: foundry.utils.duplicate(this.actor.getArchetype()),
equipements: foundry.utils.duplicate(this.actor.getEquipements()),
subActors: foundry.utils.duplicate(this.actor.getSubActors()),
phyMalus: this.actor.getPhysiqueMalus(),
elementsbio: this.actor.getElementsBio(),
sorts: this.actor.getSorts(),
description: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.object.system.biodata.description, { async: true }),
notes: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.object.system.biodata.notes, { async: true }),
equipementlibre: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.object.system.equipementlibre, { async: true }),
options: this.options,
owner: this.document.isOwner,
editScore: this.options.editScore,

View File

@@ -29,7 +29,7 @@ export class MaleficesActor extends Actor {
if (data instanceof Array) {
return super.create(data, options);
}
// If the created actor has items (only applicable to duplicated actors) bypass the new actor creation logic
// If the created actor has items (only applicable to foundry.utils.duplicated actors) bypass the new actor creation logic
if (data.items) {
let actor = super.create(data, options);
return actor;
@@ -86,8 +86,13 @@ export class MaleficesActor extends Actor {
MaleficesUtility.sortArrayObjectsByName(comp)
return comp;
}
getSorts() {
let comp = this.items.filter(item => item.type == 'sortilege');
MaleficesUtility.sortArrayObjectsByName(comp)
return comp;
}
getArchetype() {
let comp = duplicate(this.items.find(item => item.type == 'archetype') || {name: "Pas d'archetype"})
let comp = foundry.utils.duplicate(this.items.find(item => item.type == 'archetype') || {name: "Pas d'archetype"})
if (comp && comp.system) {
comp.tarot = MaleficesUtility.getTarot(comp.system.lametutelaire)
}
@@ -95,14 +100,26 @@ export class MaleficesActor extends Actor {
return comp;
}
/* -------------------------------------------- */
getElementsBio() {
let comp = foundry.utils.duplicate(this.items.filter(item => item.type == 'elementbio') || [])
MaleficesUtility.sortArrayObjectsByName(comp)
return comp;
}
/* -------------------------------------------- */
getTarots() {
let comp = duplicate(this.items.filter(item => item.type == 'tarot') || [])
let comp = foundry.utils.duplicate(this.items.filter(item => item.type == 'tarot' && !item.system.isgm) || [])
MaleficesUtility.sortArrayObjectsByName(comp)
return comp;
}
/* -------------------------------------------- */
getHiddenTarots() {
let comp = foundry.utils.duplicate(this.items.filter(item => item.type == 'tarot' && item.system.isgm) || [])
MaleficesUtility.sortArrayObjectsByName(comp)
return comp;
}
/* -------------------------------------------- */
getArmes() {
let comp = duplicate(this.items.filter(item => item.type == 'arme') || [])
let comp = foundry.utils.duplicate(this.items.filter(item => item.type == 'arme') || [])
MaleficesUtility.sortArrayObjectsByName(comp)
return comp;
}
@@ -110,7 +127,7 @@ export class MaleficesActor extends Actor {
getItemById(id) {
let item = this.items.find(item => item.id == id);
if (item) {
item = duplicate(item)
item = foundry.utils.duplicate(item)
}
return item;
}
@@ -156,7 +173,7 @@ export class MaleficesActor extends Actor {
/* ------------------------------------------- */
async buildContainerTree() {
let equipments = duplicate(this.items.filter(item => item.type == "equipment") || [])
let equipments = foundry.utils.duplicate(this.items.filter(item => item.type == "equipment") || [])
for (let equip1 of equipments) {
if (equip1.system.iscontainer) {
equip1.system.contents = []
@@ -212,22 +229,22 @@ export class MaleficesActor extends Actor {
}
/* -------------------------------------------- */
getInitiativeScore(combatId, combatantId) {
let init = Math.floor(this.system.attributs.physique.value+this.system.attributs.habilete.value)
let init = Math.floor( (this.system.attributs.physique.value+this.system.attributs.habilite.value) / 2)
let subvalue = new Roll("1d20").roll({async: false})
return init + (subvalue / 100)
return init + (subvalue.total / 100)
}
/* -------------------------------------------- */
getSubActors() {
let subActors = [];
for (let id of this.system.subactors) {
subActors.push(duplicate(game.actors.get(id)))
subActors.push(foundry.utils.duplicate(game.actors.get(id)))
}
return subActors;
}
/* -------------------------------------------- */
async addSubActor(subActorId) {
let subActors = duplicate(this.system.subactors);
let subActors = foundry.utils.duplicate(this.system.subactors);
subActors.push(subActorId);
await this.update({ 'system.subactors': subActors });
}
@@ -255,7 +272,29 @@ export class MaleficesActor extends Actor {
await this.createEmbeddedDocuments('Item', [newItem]);
}
}
/* -------------------------------------------- */
incDecFluide(value) {
let fluide = this.system.fluide + value
this.update( {'system.fluide': fluide} )
}
incDecDestin(value) {
let destin = this.system.pointdestin + value
this.update( {'system.pointdestin': destin} )
}
incDecMPMB(value) {
let mpmb = this.system.mpmb + value
this.update( {'system.mpmb': mpmb} )
}
incDecMPMN(value) {
let mpmn = this.system.mpmn + value
this.update( {'system.mpmn': mpmn} )
}
/* -------------------------------------------- */
incDecAttr(attrKey, value) {
let attr = foundry.utils.duplicate(this.system.attributs[attrKey])
attr.value += value
this.update( { [`system.attributs.${attrKey}`]: attr})
}
/* -------------------------------------------- */
async incDecQuantity(objetId, incDec = 0) {
let objetQ = this.items.get(objetId)
@@ -297,8 +336,12 @@ export class MaleficesActor extends Actor {
rollData.actorId = this.id
rollData.img = this.img
rollData.phyMalus = this.getPhysiqueMalus()
rollData.elementsbio = this.getElementsBio()
rollData.destin = this.system.pointdestin
rollData.isReroll = false
rollData.confrontationDegre = 0
rollData.confrontationModif = 0
rollData.config = game.system.malefices.config
console.log("ROLLDATA", rollData)
@@ -316,7 +359,7 @@ export class MaleficesActor extends Actor {
rollAttribut(attrKey) {
let attr = this.system.attributs[attrKey]
let rollData = this.getCommonRollData()
rollData.attr = duplicate(attr)
rollData.attr = foundry.utils.duplicate(attr)
rollData.mode = "attribut"
rollData.title = attr.label
rollData.img = this.getAtttributImage(attrKey)
@@ -327,12 +370,12 @@ export class MaleficesActor extends Actor {
rollArme(weaponId) {
let arme = this.items.get(weaponId)
if (arme) {
arme = duplicate(arme)
arme = foundry.utils.duplicate(arme)
let rollData = this.getCommonRollData()
if (arme.system.armetype == "mainsnues" || arme.system.armetype == "epee") {
rollData.attr = { label: "(Physique+Habilité)/2", value: Math.floor( (this.getPhysiqueMalus()+this.system.attributs.physique+this.system.attributs.habilite) / 2) }
rollData.attr = { label: "(Physique+Habilité)/2", value: Math.floor( (this.getPhysiqueMalus()+this.system.attributs.physique.value+this.system.attributs.habilite.value) / 2) }
} else {
rollData.attr = duplicate(this.system.attributs.habilite)
rollData.attr = foundry.utils.duplicate(this.system.attributs.habilite)
}
rollData.mode = "arme"
rollData.arme = arme
@@ -346,8 +389,7 @@ export class MaleficesActor extends Actor {
/* -------------------------------------------- */
async startRoll(rollData) {
let rollDialog = await MaleficesRollDialog.create(this, rollData)
rollDialog.render(true)
await MaleficesRollDialog.create(this, rollData)
}
}

View File

@@ -2,6 +2,8 @@
import { MaleficesUtility } from "./malefices-utility.js";
import { MaleficesRollDialog } from "./malefices-roll-dialog.js";
import { MaleficesTirageTarotDialog } from "./malefices-tirage-tarot-dialog.js"
import { MaleficesCharacterSummary } from "./malefices-summary-app.js"
/* -------------------------------------------- */
export class MaleficesCommands {
@@ -9,8 +11,9 @@ export class MaleficesCommands {
static init() {
if (!game.system.malefices.commands) {
const commands = new MaleficesCommands();
//crucibleCommands.registerCommand({ path: ["/char"], func: (content, msg, params) => crucibleCommands.createChar(msg), descr: "Create a new character" });
//crucibleCommands.registerCommand({ path: ["/pool"], func: (content, msg, params) => crucibleCommands.poolRoll(msg), descr: "Generic Roll Window" });
commands.registerCommand({ path: ["/tirage"], func: (content, msg, params) => MaleficesCommands.createTirage(msg), descr: "Tirage des tarots" });
commands.registerCommand({ path: ["/carte"], func: (content, msg, params) => MaleficesCommands.tirerCarte(msg), descr: "Tirer une carte" });
commands.registerCommand({ path: ["/resume"], func: (content, msg, params) => MaleficesCharacterSummary.displayPCSummary(), descr: "Affiche la liste des PJs!" });
game.system.malefices.commands = commands;
}
}
@@ -77,7 +80,7 @@ export class MaleficesCommands {
console.log("===> Processing command")
let command = commandsTable[name];
path = path + name + " ";
if (command && command.subTable) {
if (command?.subTable) {
if (params[0]) {
return this._processCommand(command.subTable, params[0], params.slice(1), content, msg, path)
}
@@ -86,7 +89,7 @@ export class MaleficesCommands {
return true;
}
}
if (command && command.func) {
if (command?.func) {
const result = command.func(content, msg, params);
if (result == false) {
CrucibleCommands._chatAnswer(msg, command.descr);
@@ -103,15 +106,41 @@ export class MaleficesCommands {
ChatMessage.create(msg);
}
/* -------------------------------------------- */
async poolRoll( msg) {
let rollData = MaleficesUtility.getBasicRollData()
rollData.alias = "Dice Pool Roll",
rollData.mode = "generic"
rollData.title = `Dice Pool Roll`;
/* --------------------------------------------- */
static async createTirage(msg) {
if (game.user.isGM) {
let tirageData = {
state: 'select-player',
nbCard: 0,
maxPlayerCard: 4,
maxSecretCard: 1,
cards: [],
players: foundry.utils.duplicate(game.users),
secretCards: [],
deck: MaleficesUtility.getTarots()
}
for (let i = 0; i < 4; i++) {
tirageData.cards.push({ name: "???", img: "systems/fvtt-malefices/images/tarots/background.webp" })
}
tirageData.secretCards.push({ name: "???", img: "systems/fvtt-malefices/images/tarots/background.webp" })
let rollDialog = await MaleficesRollDialog.create( this, rollData);
rollDialog.render( true );
let tirageDialog = await MaleficesTirageTarotDialog.create(this, tirageData)
}
}
/* --------------------------------------------- */
static async tirerCarte(msg) {
let deck = MaleficesUtility.getTarots()
let index = Math.round(Math.random() * (deck.length-1))
let selectedCard = deck[index]
selectedCard.system.ispositif = true
if ( selectedCard.system.isdualside) { // Cas des cartes pouvant avoir 2 sens
selectedCard.system.ispositif = (Math.random() > 0.5)
}
selectedCard.system.isgm = false
selectedCard.value = (selectedCard.system.ispositif)? selectedCard.system.numericvalueup : selectedCard.system.numericvaluedown
MaleficesUtility.createChatMessage(game.user.name, "", {
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-malefices/templates/chat/display-tarot-card.hbs`, selectedCard)
})
}
}

View File

@@ -1,5 +1,14 @@
export const MALEFICES_CONFIG = {
attributs: {
"constitution": "Cons",
"physique": "Phy",
"culturegenerale": "CGén",
"habilite": "Hab",
"perception": "Per",
"spiritualite": "Spi",
"rationnalite": "Rat"
},
tarotType: {
"majeur": "Arcane Majeur",
@@ -18,4 +27,41 @@ export const MALEFICES_CONFIG = {
"epee": "Epée, sabre, javelot, etc",
"mainsnues": "Mains Nues"
},
confrontationDegreOptions :{
"0": "0",
"1": "1",
"2": "2",
"3": "3",
"4": "4",
"5": "5"
},
confrontationModifOptions: {
"-1": "-1",
"0": "0",
"1": "+1"
},
bonusMalusPersoOptions: [
{value: "-3", label: "-3"},
{value: "-2", label: "-2"},
{value: "-1", label: "-1"},
{value: "0", label: "0"},
{value: "+1", label: "+1"},
{value: "+2", label: "+2"},
{value: "+3", label: "+3"}
],
bonusMalusDefOptions: [
{value: "-6", label: "-6 (réussite critique)"},
{value: "-3", label: "-3 (réussite)"},
{value: "0", label: "0 (echec ou pas d'esquive)"},
{value: "+3", label: "+3 (echec critique)"}
],
bonusMalusPorteeOptions: [
{value: "1", label: "+1 (Portée courte)"},
{value: "0", label: "0 (Portée moyenne)"},
{value: "-1", label: "-1 (Portée longue)"}
]
}

View File

@@ -4,12 +4,12 @@ import { MaleficesUtility } from "./malefices-utility.js";
* Extend the basic ItemSheet with some very simple modifications
* @extends {ItemSheet}
*/
export class MaleficesItemSheet extends ItemSheet {
export class MaleficesItemSheet extends foundry.appv1.sheets.ItemSheet {
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["fvtt-malefices", "sheet", "item"],
template: "systems/fvtt-malefices/templates/item-sheet.hbs",
dragDrop: [{ dragSelector: null, dropSelector: null }],
@@ -19,20 +19,6 @@ export class MaleficesItemSheet extends ItemSheet {
});
}
/* -------------------------------------------- */
_getHeaderButtons() {
let buttons = super._getHeaderButtons();
// Add "Post to chat" button
// We previously restricted this to GM and editable items only. If you ever find this comment because it broke something: eh, sorry!
buttons.unshift(
{
class: "post",
icon: "fas fa-comment",
onclick: ev => { }
})
return buttons
}
/* -------------------------------------------- */
/** @override */
setPosition(options = {}) {
@@ -57,13 +43,13 @@ export class MaleficesItemSheet extends ItemSheet {
name: this.object.name,
editable: this.isEditable,
cssClass: this.isEditable ? "editable" : "locked",
system: duplicate(this.object.system),
config: duplicate(game.system.malefices.config),
system: foundry.utils.duplicate(this.object.system),
config: foundry.utils.duplicate(game.system.malefices.config),
limited: this.object.limited,
options: this.options,
owner: this.document.isOwner,
description: await TextEditor.enrichHTML(this.object.system.description, { async: true }),
notes: await TextEditor.enrichHTML(this.object.system.notes, { async: true }),
description: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.object.system.description, { async: true }),
notes: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.object.system.notes, { async: true }),
isGM: game.user.isGM
}
@@ -90,24 +76,18 @@ export class MaleficesItemSheet extends ItemSheet {
/* -------------------------------------------- */
postItem() {
let chatData = duplicate(this.item)
let chatData = foundry.utils.duplicate(this.item)
if (this.actor) {
chatData.actor = { id: this.actor.id };
}
// Don't post any image for the item (which would leave a large gap) if the default image is used
if (chatData.img.includes("/blank.png")) {
if (chatData.img?.includes("/blank.png")) {
chatData.img = null;
}
// JSON object for easy creation
chatData.jsondata = JSON.stringify(
{
compendium: "postedItem",
payload: chatData,
});
chatData.config = game.system.malefices.config
chatData.jsondata = JSON.stringify({ compendium: "postedItem", payload: chatData })
renderTemplate('systems/Malefices/templates/post-item.html', chatData).then(html => {
let chatOptions = MaleficesUtility.chatDataSetup(html);
ChatMessage.create(chatOptions)
foundry.applications.handlebars.renderTemplate('systems/fvtt-malefices/templates/post-item.hbs', chatData).then(html => {
ChatMessage.create(MaleficesUtility.chatDataSetup(html))
});
}

View File

@@ -1,8 +1,12 @@
import { MaleficesUtility } from "./malefices-utility.js";
export const defaultItemImg = {
//skill: "systems/fvtt-malefices/images/icons/skill1.webp",
arme: "systems/fvtt-malefices/images/icones/arme.webp"
arme: "systems/fvtt-malefices/images/icons/epee.webp",
equipement: "systems/fvtt-malefices/images/icons/equipement.webp",
elementbio: "systems/fvtt-malefices/images/icons/wisdom.webp",
archetype: "systems/fvtt-malefices/images/icons/archetype.webp",
tarot: "systems/fvtt-malefices/images/icons/tarot.webp",
sortilege: "systems/fvtt-malefices/images/icons/sortilege.webp",
}
/**

View File

@@ -9,14 +9,19 @@
/* -------------------------------------------- */
// Import Modules
import { MaleficesActor } from "./malefices-actor.js";
import { MaleficesItemSheet } from "./malefices-item-sheet.js";
import { MaleficesActorSheet } from "./malefices-actor-sheet.js";
import { MaleficesNPCSheet } from "./malefices-npc-sheet.js";
import { MaleficesUtility } from "./malefices-utility.js";
import { MaleficesCombat } from "./malefices-combat.js";
import { MaleficesItem } from "./malefices-item.js";
import { MaleficesHotbar } from "./malefices-hotbar.js"
import { MaleficesCharacterSummary } from "./malefices-summary-app.js"
import { MALEFICES_CONFIG } from "./malefices-config.js"
import { ClassCounter} from "https://www.uberwald.me/fvtt_appcount/count-class-ready.js"
// Import DataModels
import * as models from "./models/index.mjs"
// Import AppV2 Sheets
import * as sheets from "./applications/sheets/_module.mjs"
/* -------------------------------------------- */
/* Foundry VTT Initialization */
@@ -52,31 +57,52 @@ Hooks.once("init", async function () {
// Define custom Entity classes
CONFIG.Combat.documentClass = MaleficesCombat
CONFIG.Actor.documentClass = MaleficesActor
CONFIG.Actor.dataModels = {
personnage: models.PersonnageDataModel,
pnj: models.PnjDataModel
}
CONFIG.Item.documentClass = MaleficesItem
CONFIG.Item.dataModels = {
arme: models.ArmeDataModel,
equipement: models.EquipementDataModel,
archetype: models.ArchetypeDataModel,
tarot: models.TarotDataModel,
sortilege: models.SortilegeDataModel,
elementbio: models.ElementbioDataModel
}
/* -------------------------------------------- */
// Register sheet application classes
Actors.unregisterSheet("core", ActorSheet);
Actors.registerSheet("fvtt-malefices", MaleficesActorSheet, { types: ["personnage"], makeDefault: true });
Actors.registerSheet("fvtt-malefices", MaleficesNPCSheet, { types: ["pnj"], makeDefault: false });
// Register AppV2 Actor Sheets
foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet);
foundry.documents.collections.Actors.registerSheet("fvtt-malefices", sheets.MaleficesPersonnageSheet, { types: ["personnage"], makeDefault: true });
foundry.documents.collections.Actors.registerSheet("fvtt-malefices", sheets.MaleficesNPCActorSheet, { types: ["pnj"], makeDefault: true });
Items.unregisterSheet("core", ItemSheet);
Items.registerSheet("fvtt-malefices", MaleficesItemSheet, { makeDefault: true });
// Register AppV2 Item Sheets
foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ItemSheet);
foundry.documents.collections.Items.registerSheet("fvtt-malefices", sheets.MaleficesArmeSheet, { types: ["arme"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-malefices", sheets.MaleficesEquipementSheet, { types: ["equipement"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-malefices", sheets.MaleficesArchetypeSheet, { types: ["archetype"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-malefices", sheets.MaleficesTarotSheet, { types: ["tarot"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-malefices", sheets.MaleficesSortilegeSheet, { types: ["sortilege"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-malefices", sheets.MaleficesElementbioSheet, { types: ["elementbio"], makeDefault: true });
MaleficesUtility.init()
});
/* -------------------------------------------- */
function welcomeMessage() {
async function welcomeMessage() {
if (game.user.isGM) {
const content = await foundry.applications.handlebars.renderTemplate(
'systems/fvtt-malefices/templates/chat/welcome-message.hbs', {}
)
ChatMessage.create({
user: game.user.id,
whisper: [game.user.id],
content: `<div id="welcome-message-malefices"><span class="rdd-roll-part">
<strong>Bienvenu dans Malefices, le JDR qui sent le souffre !</strong>
<p>Le Livre de Base de Maléfices v4 est nécessaire pour jouer : https://arkhane-asylum.fr/en/malefices/</p>
<p>Maléfices et un jeu de rôle publié par Arkhane Asylum Publishing, tout les droits leur appartiennent.</p>
<p>Système développé par LeRatierBretonnien avec l'aide de la Dame du Lac et Malik, support sur le <a href="https://discord.gg/pPSDNJk">Discord FR de Foundry</a>.</p>
` });
content
});
}
}
/* -------------------------------------------- */
@@ -93,17 +119,14 @@ Hooks.once("ready", function () {
});
}
// CSS patch for v9
if (game.version) {
let sidebar = document.getElementById("sidebar");
sidebar.style.width = "min-content";
}
ClassCounter.registerUsageCount();
welcomeMessage();
MaleficesUtility.ready()
MaleficesUtility.init()
MaleficesCharacterSummary.ready()
})
/* -------------------------------------------- */
/* Foundry VTT Initialization */
/* -------------------------------------------- */
@@ -111,10 +134,9 @@ Hooks.on("chatMessage", (html, content, msg) => {
if (content[0] == '/') {
let regExp = /(\S+)/g;
let commands = content.match(regExp);
if (game.system.Malefices.commands.processChatCommand(commands, content, msg)) {
if (game.system.malefices.commands.processChatCommand(commands, content, msg)) {
return false;
}
}
return true;
});

View File

@@ -6,12 +6,12 @@
import { MaleficesUtility } from "./malefices-utility.js";
/* -------------------------------------------- */
export class MaleficesNPCSheet extends ActorSheet {
export class MaleficesNPCSheet extends foundry.appv1.sheets.ActorSheet {
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["Malefices", "sheet", "actor"],
template: "systems/fvtt-malefices/templates/npc-sheet.html",
width: 640,
@@ -25,7 +25,7 @@ export class MaleficesNPCSheet extends ActorSheet {
/* -------------------------------------------- */
async getData() {
const objectData = this.object.system
let actorData = duplicate(objectData)
let actorData = foundry.utils.duplicate(objectData)
let formData = {
title: this.title,
@@ -38,16 +38,16 @@ export class MaleficesNPCSheet extends ActorSheet {
data: actorData,
limited: this.object.limited,
skills: this.actor.getSkills( ),
weapons: this.actor.checkAndPrepareEquipments( duplicate(this.actor.getWeapons()) ),
armors: this.actor.checkAndPrepareEquipments( duplicate(this.actor.getArmors())),
shields: this.actor.checkAndPrepareEquipments( duplicate(this.actor.getShields())),
spells: this.actor.checkAndPrepareEquipments( duplicate(this.actor.getLore())),
equipments: this.actor.checkAndPrepareEquipments(duplicate(this.actor.getEquipmentsOnly()) ),
equippedWeapons: this.actor.checkAndPrepareEquipments(duplicate(this.actor.getEquippedWeapons()) ),
weapons: this.actor.checkAndPrepareEquipments( foundry.utils.duplicate(this.actor.getWeapons()) ),
armors: this.actor.checkAndPrepareEquipments( foundry.utils.duplicate(this.actor.getArmors())),
shields: this.actor.checkAndPrepareEquipments( foundry.utils.duplicate(this.actor.getShields())),
spells: this.actor.checkAndPrepareEquipments( foundry.utils.duplicate(this.actor.getLore())),
equipments: this.actor.checkAndPrepareEquipments(foundry.utils.duplicate(this.actor.getEquipmentsOnly()) ),
equippedWeapons: this.actor.checkAndPrepareEquipments(foundry.utils.duplicate(this.actor.getEquippedWeapons()) ),
equippedArmor: this.actor.getEquippedArmor(),
equippedShield: this.actor.getEquippedShield(),
subActors: duplicate(this.actor.getSubActors()),
moneys: duplicate(this.actor.getMoneys()),
subActors: foundry.utils.duplicate(this.actor.getSubActors()),
moneys: foundry.utils.duplicate(this.actor.getMoneys()),
encCapacity: this.actor.getEncumbranceCapacity(),
saveRolls: this.actor.getSaveRoll(),
conditions: this.actor.getConditions(),

View File

@@ -1,75 +1,57 @@
import { MaleficesUtility } from "./malefices-utility.js";
export class MaleficesRollDialog extends Dialog {
export class MaleficesRollDialog {
/* -------------------------------------------- */
static async create(actor, rollData) {
const isCard = rollData?.attr?.iscard
const template = isCard
? 'systems/fvtt-malefices/templates/dialogs/confrontation-dialog.hbs'
: 'systems/fvtt-malefices/templates/dialogs/roll-dialog-generic.hbs'
let options = { classes: ["MaleficesDialog"], width: 540, height: 'fit-content', 'z-index': 99999 };
let html = await renderTemplate('systems/fvtt-malefices/templates/dialogs/roll-dialog-generic.hbs', rollData);
const content = await foundry.applications.handlebars.renderTemplate(template, rollData)
return new MaleficesRollDialog(actor, rollData, html, options);
}
/* -------------------------------------------- */
constructor(actor, rollData, html, options, close = undefined) {
let conf = {
title: (rollData.mode == "skill") ? "Skill" : "Attribute",
content: html,
buttons: {
roll: {
icon: '<i class="fas fa-check"></i>',
label: "Roll !",
callback: () => { this.roll() }
return foundry.applications.api.DialogV2.wait({
window: {
title: isCard ? "Tirage" : "Jet de dé",
icon: isCard ? "fa-solid fa-layer-group" : "fa-solid fa-dice-d20",
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: "Cancel",
callback: () => { this.close() }
classes: ["malefices-roll-dialog"],
position: { width: 540 },
modal: false,
rejectClose: false,
content,
buttons: [
{
action: "roll",
label: isCard ? "Tirer une carte" : "Lancer le dé",
icon: isCard ? "fa-solid fa-layer-group" : "fa-solid fa-check",
default: true,
callback: (event, button, dialog) => {
MaleficesRollDialog._updateRollDataFromForm(rollData, button.form.elements)
if (isCard) {
MaleficesUtility.tirageConfrontationMalefices(rollData)
} else {
MaleficesUtility.rollMalefices(rollData)
}
}
},
close: close
{
action: "cancel",
label: "Annuler",
icon: "fa-solid fa-times",
}
super(conf, options);
this.actor = actor;
this.rollData = rollData;
],
})
}
/* -------------------------------------------- */
roll() {
MaleficesUtility.rollMalefices(this.rollData)
}
/* -------------------------------------------- */
async refreshDialog() {
const content = await renderTemplate("systems/fvtt-malefices/templates/dialogs/roll-dialog-generic.hbs", this.rollData)
this.data.content = content
this.render(true)
}
/* -------------------------------------------- */
activateListeners(html) {
super.activateListeners(html);
var dialog = this;
function onLoad() {
}
$(function () { onLoad(); });
html.find('#bonusMalusSituation').change((event) => {
this.rollData.bonusMalusSituation = Number(event.currentTarget.value)
})
html.find('#bonusMalusPerso').change((event) => {
this.rollData.bonusMalusPerso = Number(event.currentTarget.value)
})
html.find('#bonusMalusDef').change((event) => {
this.rollData.bonusMalusDef = Number(event.currentTarget.value)
})
html.find('#bonusMalusPortee').change((event) => {
this.rollData.bonusMalusPortee = Number(event.currentTarget.value)
})
static _updateRollDataFromForm(rollData, elements) {
if (elements.bonusMalusPerso) rollData.bonusMalusPerso = Number(elements.bonusMalusPerso.value)
if (elements.bonusMalusSituation) rollData.bonusMalusSituation = Number(elements.bonusMalusSituation.value)
if (elements.bonusMalusDef) rollData.bonusMalusDef = Number(elements.bonusMalusDef.value)
if (elements.bonusMalusPortee) rollData.bonusMalusPortee = Number(elements.bonusMalusPortee.value)
if (elements.confrontationDegre) rollData.confrontationDegre = Number(elements.confrontationDegre.value)
if (elements.confrontationModif) rollData.confrontationModif = Number(elements.confrontationModif.value)
}
}

View File

@@ -0,0 +1,133 @@
/* -------------------------------------------- */
import { MaleficesUtility } from "./malefices-utility.js";
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api
/* -------------------------------------------- */
export class MaleficesCharacterSummary extends HandlebarsApplicationMixin(ApplicationV2) {
/* -------------------------------------------- */
static DEFAULT_OPTIONS = {
id: "malefices-character-summary",
classes: ["MaleficesDialog"],
window: { title: "Résumé des Personnages", resizable: true },
position: { width: 960, height: "auto" },
dragDrop: [{ dragSelector: null, dropSelector: ".character-summary-container" }],
}
static PARTS = {
form: { template: "systems/fvtt-malefices/templates/dialogs/character-summary.hbs" }
}
/* -------------------------------------------- */
static displayPCSummary() {
if (game.user.isGM) {
game.system.malefices.charSummary.render({ force: true })
} else {
ui.notifications.info("Commande /resume réservée au MJ !")
}
}
/* -------------------------------------------- */
updatePCSummary() {
if (this.element?.isConnected) {
this.render({ force: true })
}
}
/* -------------------------------------------- */
static ready() {
if (!game.user.isGM) {
return
}
game.system.malefices.charSummary = new MaleficesCharacterSummary()
}
/* -------------------------------------------- */
constructor(options = {}) {
super(options)
const saved = game.settings.get("world", "character-summary-data")
this.extraList = saved.extraList ?? saved.npcList ?? []
}
/* -------------------------------------------- */
async _prepareContext(_options) {
const pcs = game.actors.filter(ac => ac.type === "personnage" && ac.hasPlayerOwner)
const extras = []
const validList = []
for (const actorId of this.extraList) {
const actor = game.actors.get(actorId)
if (actor) { extras.push(actor); validList.push(actorId) }
}
if (validList.length !== this.extraList.length) {
this.extraList = validList
this._persist()
}
return { pcs, extras, config: game.system.malefices.config }
}
/* -------------------------------------------- */
_onRender(_context, _options) {
const el = this.element
el.querySelectorAll('.actor-open').forEach(a => {
a.addEventListener('click', event => {
const li = event.currentTarget.closest('.item')
const actor = game.actors.get(li.dataset.actorId)
actor?.sheet.render(true)
})
})
el.querySelectorAll('.summary-roll').forEach(a => {
a.addEventListener('click', event => {
const li = event.currentTarget.closest('.item')
const actor = game.actors.get(li.dataset.actorId)
const key = event.currentTarget.dataset.key
actor?.rollAttribut(key)
})
})
el.querySelectorAll('.actor-delete').forEach(a => {
a.addEventListener('click', event => {
const li = event.currentTarget.closest('.item')
this.extraList = this.extraList.filter(id => id !== li.dataset.actorId)
this._persist()
this.render({ force: true })
})
})
const dropZone = el.querySelector('.character-summary-container')
if (dropZone) {
dropZone.addEventListener('dragover', ev => ev.preventDefault())
dropZone.addEventListener('drop', ev => { ev.stopPropagation(); this._onDrop(ev) })
}
}
/* -------------------------------------------- */
_canDragDrop(_selector) { return true }
/* -------------------------------------------- */
_persist() {
const saved = game.settings.get("world", "character-summary-data")
game.settings.set("world", "character-summary-data", { ...saved, extraList: this.extraList })
}
/* -------------------------------------------- */
_saveAndRefresh() {
this.render({ force: true })
}
/* -------------------------------------------- */
async _onDrop(event) {
try {
const dataItem = JSON.parse(event.dataTransfer.getData('text/plain'))
const actor = fromUuidSync(dataItem.uuid)
if (actor && !this.extraList.includes(actor.id)) {
this.extraList.push(actor.id)
this._persist()
this.render({ force: true })
}
} catch(e) { /* not a valid drag payload */ }
}
}

View File

@@ -0,0 +1,132 @@
import { MaleficesUtility } from "./malefices-utility.js";
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api
export class MaleficesTirageTarotDialog extends HandlebarsApplicationMixin(ApplicationV2) {
/* -------------------------------------------- */
static DEFAULT_OPTIONS = {
id: "malefices-tirage-tarot",
classes: ["MaleficesDialog"],
window: { title: "Tirage des Tarots", resizable: true },
position: { width: 720, height: 740 },
}
static PARTS = {
form: { template: 'systems/fvtt-malefices/templates/dialogs/tirage-tarot-dialog.hbs' }
}
/* -------------------------------------------- */
constructor(actor, tirageData, options = {}) {
super(options)
this.actor = actor
this.tirageData = tirageData
}
/* -------------------------------------------- */
static async create(actor, tirageData) {
const app = new MaleficesTirageTarotDialog(actor, tirageData)
app.render({ force: true })
return app
}
/* -------------------------------------------- */
async _prepareContext(_options) {
return { ...this.tirageData }
}
/* -------------------------------------------- */
_onRender(_context, _options) {
const el = this.element
el.querySelector('#playerId')?.addEventListener('change', (event) => {
if (event.currentTarget.value !== "none") {
this.tirageData.playerId = event.currentTarget.value
this.processSelectedPlayer()
}
})
el.querySelector('#actorId')?.addEventListener('change', (event) => {
if (event.currentTarget.value !== "none") {
this.attributeToActor(event.currentTarget.value)
}
})
el.querySelector('.tirage-close-btn')?.addEventListener('click', () => this.close())
}
/* -------------------------------------------- */
async sendCardRequest() {
this.tirageData.state = 'waiting-user-card'
await MaleficesUtility.createChatMessage(this.tirageData.user.name, "useronly", {
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-malefices/templates/chat/request-tarot-card.hbs`, this.tirageData)
})
}
/* -------------------------------------------- */
drawCard() {
let index = Math.round(Math.random() * (this.tirageData.deck.length - 1))
let selectedCard = this.tirageData.deck[index]
selectedCard.system.ispositif = true
if (selectedCard.system.isdualside) {
selectedCard.system.ispositif = (Math.random() > 0.5)
}
// Cas spécial de la Roue de la Fortune
if (selectedCard.name.toLowerCase().includes("fortune")) {
this.tirageData.maxPlayerCard += 1
this.tirageData.maxSecretCard += 1
}
this.tirageData.deck = this.tirageData.deck.filter(c => c.name !== selectedCard.name)
return selectedCard
}
/* -------------------------------------------- */
async addCard(msgId) {
MaleficesUtility.removeChatMessageId(msgId)
let selectedCard = this.drawCard()
selectedCard.system.isgm = false
await MaleficesUtility.createChatMessage(this.tirageData.user.name, "gmroll", {
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-malefices/templates/chat/display-tarot-card.hbs`, selectedCard)
})
if (this.tirageData.cards[0].name == "???") this.tirageData.cards.shift()
this.tirageData.cards.push(selectedCard)
this.tirageData.nbCard++
if (this.tirageData.nbCard == this.tirageData.maxPlayerCard) {
for (let i = 0; i < this.tirageData.maxSecretCard; i++) {
let secretCard = this.drawCard()
secretCard.system.isgm = true
await MaleficesUtility.createChatMessage(this.tirageData.user.name, "blindroll", {
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-malefices/templates/chat/display-tarot-card.hbs`, secretCard)
})
if (this.tirageData.secretCards[0].name == "???") this.tirageData.secretCards.shift()
this.tirageData.secretCards.push(secretCard)
}
this.tirageData.actors = foundry.utils.duplicate(game.actors)
this.tirageData.state = 'attribute-to-actor'
} else {
this.sendCardRequest()
}
this.render({ force: true })
}
/* -------------------------------------------- */
async processSelectedPlayer() {
this.tirageData.user = game.users.get(this.tirageData.playerId)
this.tirageData.players = null
game.system.malefices.currentTirage = this
this.render({ force: true })
this.sendCardRequest()
}
/* -------------------------------------------- */
attributeToActor(actorId) {
const actor = game.actors.get(actorId)
if (actor) {
actor.createEmbeddedDocuments('Item', this.tirageData.cards)
actor.createEmbeddedDocuments('Item', this.tirageData.secretCards)
ui.notifications.info("Les cartes ont été attribuées à " + actor.name)
}
}
}

View File

@@ -1,23 +1,22 @@
/* -------------------------------------------- */
import { MaleficesCombat } from "./malefices-combat.js";
import { MaleficesCommands } from "./malefices-commands.js";
/* -------------------------------------------- */
export class MaleficesUtility {
/* -------------------------------------------- */
static async init() {
Hooks.on('renderChatLog', (log, html, data) => MaleficesUtility.chatListeners(html));
/*Hooks.on("dropCanvasData", (canvas, data) => {
MaleficesUtility.dropItemOnToken(canvas, data)
});*/
this.rollDataStore = {}
this.defenderStore = {}
MaleficesCommands.init();
}
/* -------------------------------------------- */
static async ready() {
Handlebars.registerHelper('count', function (list) {
return list.length;
@@ -44,6 +43,26 @@ export class MaleficesUtility {
Handlebars.registerHelper('add', function (a, b) {
return parseInt(a) + parseInt(b);
})
// Handle v12 removal of this helper
Handlebars.registerHelper('select', function (selected, options) {
const escapedValue = RegExp.escape(Handlebars.escapeExpression(selected));
const rgx = new RegExp(' value=[\"\']' + escapedValue + '[\"\']');
const html = options.fn(this);
return html.replace(rgx, "$& selected");
});
game.settings.register("world", "character-summary-data", {
name: "character-summary-data",
scope: "world",
config: false,
default: { extraList: [], x: 200, y: 200 },
type: Object
})
const tarots = await MaleficesUtility.loadCompendium("fvtt-malefices.malefices-tarots")
this.tarots = tarots.map(i => i.toObject())
}
/*-------------------------------------------- */
@@ -54,18 +73,12 @@ export class MaleficesUtility {
/*-------------------------------------------- */
static getTarots() {
return duplicate(this.tarots)
return foundry.utils.duplicate(this.tarots)
}
static getTarot(tId) {
return this.tarots.find(t => t._id == tId)
}
/* -------------------------------------------- */
static async ready() {
const tarots = await MaleficesUtility.loadCompendium("fvtt-malefices.malefices-tarots")
this.tarots = tarots.map(i => i.toObject())
}
/* -------------------------------------------- */
static async loadCompendiumData(compendium) {
const pack = game.packs.get(compendium)
@@ -89,11 +102,19 @@ export class MaleficesUtility {
}
return actor
}
/* -------------------------------------------- */
static drawDeckCard(msgId) {
if (game.user.isGM) {
game.system.malefices.currentTirage.addCard(msgId)
} else {
game.socket.emit("system.fvtt-malefices", { name: "msg-draw-card", data: { msgId: msgId } })
}
}
/* -------------------------------------------- */
static async chatListeners(html) {
html.on("click", '.roll-destin', event => {
$(html).on("click", '.roll-destin', event => {
let messageId = MaleficesUtility.findChatMessageId(event.currentTarget)
let message = game.messages.get(messageId)
let rollData = message.getFlag("world", "rolldata")
@@ -102,6 +123,11 @@ export class MaleficesUtility {
rollData.isReroll = true
this.rollMalefices(rollData)
})
$(html).on("click", '.draw-tarot-card', event => {
let messageId = MaleficesUtility.findChatMessageId(event.currentTarget)
this.drawDeckCard(messageId)
})
}
/* -------------------------------------------- */
@@ -109,10 +135,15 @@ export class MaleficesUtility {
const templatePaths = [
'systems/fvtt-malefices/templates/actors/editor-notes-gm.hbs',
'systems/fvtt-malefices/templates/items/partial-item-header.hbs',
'systems/fvtt-malefices/templates/items/partial-item-nav.hbs',
'systems/fvtt-malefices/templates/items/partial-item-description.hbs'
'systems/fvtt-malefices/templates/items/partial-item-description.hbs',
'systems/fvtt-malefices/templates/post-item.hbs',
'systems/fvtt-malefices/templates/actors/npc-sheet.hbs',
'systems/fvtt-malefices/templates/chat/welcome-message.hbs',
'systems/fvtt-malefices/templates/dialogs/character-summary.hbs'
]
return loadTemplates(templatePaths);
return foundry.applications.handlebars.loadTemplates(templatePaths);
}
/* -------------------------------------------- */
@@ -178,28 +209,17 @@ export class MaleficesUtility {
let id = rollData.rollId
let oldRollData = this.rollDataStore[id] || {}
let newRollData = mergeObject(oldRollData, rollData)
let newRollData = foundry.utils.mergeObject(oldRollData, rollData)
this.rollDataStore[id] = newRollData
}
/* -------------------------------------------- */
static async onSocketMesssage(msg) {
console.log("SOCKET MESSAGE", msg.name)
if (msg.name == "msg_update_roll") {
this.updateRollData(msg.data)
if (msg.name == "msg-draw-card") {
if (game.user.isGM && game.system.malefices.currentTirage) {
game.system.malefices.currentTirage.addCard(msg.data.msgId)
}
if (msg.name == "msg_gm_process_attack_defense") {
this.processSuccessResult(msg.data)
}
if (msg.name == "msg_gm_item_drop" && game.user.isGM) {
let actor = game.actors.get(msg.data.actorId)
let item
if (msg.data.isPack) {
item = await fromUuid("Compendium." + msg.data.isPack + "." + msg.data.itemId)
} else {
item = game.items.get(msg.data.itemId)
}
this.addItemDropToActor(actor, item)
}
}
@@ -261,6 +281,133 @@ export class MaleficesUtility {
}
}
/* -------------------------------------------- */
static processSpecialCard(actor, rollData) {
if (rollData.selectedCard.name.toLowerCase().includes("archange")) {
let actorCard = actor.items.find(c => c.type == "tarot" && c.name.toLowerCase().includes("archange"))
if (actorCard) {
MaleficesUtility.createChatMessage(actor.name, "gmroll", {
content: `Conséquence supplémentaire ! <br>L'Archange : ${actor.name} gagne 1 point de Spiritualité.`
})
actor.incDecAttr("spiritualite", 1)
}
}
if (rollData.selectedCard.name.toLowerCase().includes("vicaire")) {
let actorCard = actor.items.find(c => c.type == "tarot" && c.name.toLowerCase().includes("vicaire"))
if (actorCard) {
MaleficesUtility.createChatMessage(actor.name, "blindroll", {
content: `Conséquence supplémentaire ! <br>Le Vicaire : ${actor.name} vient de gagner 1 point en Pratique de la Magie Blanche (MPMB, secret).`
})
actor.incDecMPMB(1)
}
}
if (rollData.selectedCard.name.toLowerCase().includes("chance")) {
let actorCard = actor.items.find(c => c.type == "tarot" && c.name.toLowerCase().includes("chance"))
if (actorCard) {
MaleficesUtility.createChatMessage(actor.name, "gmroll", {
content: `Conséquence supplémentaire ! <br>La Chance : ${actor.name} a gagné 1 point de Destin.`
})
actor.incDecDestin(1)
}
}
if (rollData.selectedCard.name.toLowerCase().includes("mort")) {
let actorCard = actor.items.find(c => c.type == "tarot" && c.name.toLowerCase().includes("mort"))
if (actorCard) {
MaleficesUtility.createChatMessage(actor.name, "gmroll", {
content: `Conséquence supplémentaire ! <br>La Mort : ${actor.name} est pétrifié par la peur.`
})
actor.incDecDestin(1)
}
}
if (rollData.selectedCard.name.toLowerCase().includes("diable")) {
let actorCard = actor.items.find(c => c.type == "tarot" && c.name.toLowerCase().includes("diable"))
if (actorCard) {
MaleficesUtility.createChatMessage(actor.name, "gmroll", {
content: `Conséquence supplémentaire ! <br>Le Diable : ${actor.name} gagne 1 point de Rationnalité.`
})
actor.incDecAttr("rationnalite", 1)
}
}
if (rollData.selectedCard.name.toLowerCase().includes("lune noire")) {
let actorCard = actor.items.find(c => c.type == "tarot" && c.name.toLowerCase().includes("lune noire"))
if (actorCard) {
MaleficesUtility.createChatMessage(actor.name, "blindroll", {
content: `Conséquence supplémentaire ! <br>La Lune Noire : ${actor.name} vient de gagner 1 point de Fluide (secret).`
})
actor.incDecFluide(1)
}
}
if (rollData.selectedCard.name.toLowerCase().includes("grand livre")) {
let actorCard = actor.items.find(c => c.type == "tarot" && c.name.toLowerCase().includes("grand livre"))
if (actorCard) {
MaleficesUtility.createChatMessage(actor.name, "blindroll", {
content: `Conséquence supplémentaire ! <br>La Lune Noire : ${actor.name} vient de gagner 1 point de Fluide (secret).`
})
actor.incDecFluide(1)
}
}
if (rollData.selectedCard.name.toLowerCase().includes("sorcier")) {
let actorCard = actor.items.find(c => c.type == "tarot" && c.name.toLowerCase().includes("sorcier"))
if (actorCard) {
MaleficesUtility.createChatMessage(actor.name, "blindroll", {
content: `Conséquence supplémentaire ! <br>Le Vicaire : ${actor.name} vient de gagner 1 point en Pratique de la Magie Noire (MPMN, secret).`
})
actor.incDecMPMN(1)
}
}
}
/* -------------------------------------------- */
static computeResults(rollData) {
rollData.isSuccess = false
if (rollData.total <= rollData.target) {
rollData.isSuccess = true
}
if (rollData.total == 1) {
rollData.isSuccess = true
rollData.isCritical = true
}
if (rollData.total == 20) {
rollData.isSuccess = false
rollData.isFumble = true
}
if (rollData.total <= Math.floor(rollData.target / 3)) {
rollData.isPart = true
}
}
/* -------------------------------------------- */
static async tirageConfrontationMalefices(rollData) {
let actor = game.actors.get(rollData.actorId)
rollData.target = rollData.attr.value - rollData.confrontationDegre + rollData.confrontationModif
let deck = this.getTarots()
let index = Math.round(Math.random() * (deck.length - 1))
let selectedCard = deck[index]
selectedCard.system.ispositif = (Math.random() > 0.5)
selectedCard.value = (selectedCard.system.ispositif) ? selectedCard.system.numericvalueup : selectedCard.system.numericvaluedown
rollData.total = selectedCard.value
rollData.selectedCard = selectedCard
await MaleficesUtility.createChatMessage(actor.name, "gmroll", {
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-malefices/templates/chat/display-tarot-card.hbs`, selectedCard)
})
this.computeResults(rollData)
if (rollData.isSuccess) {
rollData.gainAttr = Math.ceil(rollData.confrontationDegre / 2) + ((rollData.isCritical) ? 1 : 0)
actor.incDecAttr(rollData.attr.abbrev, rollData.gainAttr)
} else {
rollData.gainAttr = rollData.confrontationDegre
actor.incDecAttr(rollData.attr.abbrev, -rollData.gainAttr)
}
await MaleficesUtility.createChatMessage(actor.name, "gmroll", {
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-malefices/templates/chat/chat-confrontation-result.hbs`, rollData)
})
this.processSpecialCard(actor, rollData)
}
/* -------------------------------------------- */
static async rollMalefices(rollData) {
@@ -276,28 +423,15 @@ export class MaleficesUtility {
// Performs roll
console.log("Roll formula", diceFormula)
let myRoll = new Roll(diceFormula).roll({ async: false })
let myRoll = await new Roll(diceFormula).roll()
await this.showDiceSoNice(myRoll, game.settings.get("core", "rollMode"))
rollData.roll = myRoll
rollData.roll = foundry.utils.duplicate(myRoll)
rollData.total = myRoll.total
rollData.isSuccess = false
if (myRoll.total <= rollData.target) {
rollData.isSuccess = true
}
if (myRoll.total == 1) {
rollData.isSuccess = true
rollData.isCritical = true
}
if (myRoll.total == 20) {
rollData.isSuccess = false
rollData.isFumble = true
}
if (myRoll.total <= Math.floor(rollData.target / 3)) {
rollData.isPart = true
}
this.computeResults(rollData)
let msg = await this.createChatWithRollMode(rollData.alias, {
content: await renderTemplate(`systems/fvtt-malefices/templates/chat/chat-generic-result.hbs`, rollData)
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-malefices/templates/chat/chat-generic-result.hbs`, rollData)
})
msg.setFlag("world", "rolldata", rollData)
if (rollData.mode == "initiative") {
@@ -331,11 +465,17 @@ export class MaleficesUtility {
switch (rollMode) {
case "blindroll": return this.getUsers(user => user.isGM);
case "gmroll": return this.getWhisperRecipientsAndGMs(name);
case "useronly": return this.getWhisperRecipientsOnly(name);
case "selfroll": return [game.user.id];
}
return undefined;
}
/* -------------------------------------------- */
static getWhisperRecipientsOnly(name) {
let recep1 = ChatMessage.getWhisperRecipients(name) || [];
return recep1
}
/* -------------------------------------------- */
static getWhisperRecipientsAndGMs(name) {
let recep1 = ChatMessage.getWhisperRecipients(name) || [];
return recep1.concat(ChatMessage.getWhisperRecipients('GM'));
@@ -343,7 +483,7 @@ export class MaleficesUtility {
/* -------------------------------------------- */
static blindMessageToGM(chatOptions) {
let chatGM = duplicate(chatOptions);
let chatGM = foundry.utils.duplicate(chatOptions);
chatGM.whisper = this.getUsers(user => user.isGM);
chatGM.content = "Blinde message of " + game.user.name + "<br>" + chatOptions.content;
console.log("blindMessageToGM", chatGM);
@@ -369,7 +509,7 @@ export class MaleficesUtility {
}
/* -------------------------------------------- */
static createChatMessage(name, rollMode, chatOptions) {
static async createChatMessage(name, rollMode, chatOptions) {
switch (rollMode) {
case "blindroll": // GM only
if (!game.user.isGM) {
@@ -387,13 +527,13 @@ export class MaleficesUtility {
break;
}
chatOptions.alias = chatOptions.alias || name;
return ChatMessage.create(chatOptions);
return await ChatMessage.create(chatOptions);
}
/* -------------------------------------------- */
static getBasicRollData() {
let rollData = {
rollId: randomID(16),
rollId: foundry.utils.randomID(16),
bonusMalusPerso: 0,
bonusMalusSituation: 0,
bonusMalusDef: 0,
@@ -413,36 +553,22 @@ export class MaleficesUtility {
}
/* -------------------------------------------- */
static createChatWithRollMode(name, chatOptions) {
return this.createChatMessage(name, game.settings.get("core", "rollMode"), chatOptions)
static async createChatWithRollMode(name, chatOptions) {
return await this.createChatMessage(name, game.settings.get("core", "rollMode"), chatOptions)
}
/* -------------------------------------------- */
static async confirmDelete(actorSheet, li) {
let itemId = li.data("item-id");
let msgTxt = "<p>Are you sure to remove this Item ?";
let buttons = {
delete: {
icon: '<i class="fas fa-check"></i>',
label: "Yes, remove it",
callback: () => {
actorSheet.actor.deleteEmbeddedDocuments("Item", [itemId]);
li.slideUp(200, () => actorSheet.render(false));
}
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: "Cancel"
}
}
msgTxt += "</p>";
let d = new Dialog({
title: "Confirm removal",
content: msgTxt,
buttons: buttons,
default: "cancel"
const itemId = li.dataset.itemId;
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: { title: "Confirmer la suppression" },
content: "<p>Supprimer cet objet ?</p>",
yes: { label: "Supprimer", icon: "fas fa-trash" },
no: { label: "Annuler", icon: "fas fa-times" },
});
d.render(true);
if (confirmed) {
actorSheet.actor.deleteEmbeddedDocuments("Item", [itemId]);
}
}
}

View File

@@ -0,0 +1,12 @@
/**
* Data model pour les archétypes
*/
export default class ArchetypeDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
lametutelaire: new fields.StringField({ initial: "" }),
description: new fields.HTMLField({ initial: "" })
};
}
}

19
modules/models/arme.mjs Normal file
View File

@@ -0,0 +1,19 @@
/**
* Data model pour les armes
*/
export default class ArmeDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
armetype: new fields.StringField({ initial: "" }),
porteecourte: new fields.StringField({ initial: "" }),
porteemoyenne: new fields.StringField({ initial: "" }),
dommagenormale: new fields.NumberField({ initial: 0, integer: true }),
dommagepart: new fields.NumberField({ initial: 0, integer: true }),
dommagecritique: new fields.NumberField({ initial: 0, integer: true }),
dommagecritiqueKO: new fields.BooleanField({ initial: false }),
dommagecritiquemort: new fields.BooleanField({ initial: false }),
description: new fields.HTMLField({ initial: "" })
};
}
}

View File

@@ -0,0 +1,11 @@
/**
* Data model pour les éléments biographiques
*/
export default class ElementbioDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" })
};
}
}

View File

@@ -0,0 +1,11 @@
/**
* Data model pour les équipements
*/
export default class EquipementDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" })
};
}
}

15
modules/models/index.mjs Normal file
View File

@@ -0,0 +1,15 @@
/**
* Index des DataModels pour Maléfices
*/
// Modèles d'acteurs
export { default as PersonnageDataModel } from './personnage.mjs';
export { default as PnjDataModel } from './pnj.mjs';
// Modèles d'items
export { default as ArmeDataModel } from './arme.mjs';
export { default as EquipementDataModel } from './equipement.mjs';
export { default as ArchetypeDataModel } from './archetype.mjs';
export { default as TarotDataModel } from './tarot.mjs';
export { default as SortilegeDataModel } from './sortilege.mjs';
export { default as ElementbioDataModel } from './elementbio.mjs';

View File

@@ -0,0 +1,95 @@
/**
* Data model pour les personnages joueurs (type "personnage")
*/
export default class PersonnageDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
// Template biodata
biodata: new fields.SchemaField({
age: new fields.NumberField({ initial: 0, integer: true }),
size: new fields.StringField({ initial: "" }),
lieunaissance: new fields.StringField({ initial: "" }),
nationalite: new fields.StringField({ initial: "" }),
profession: new fields.StringField({ initial: "" }),
residence: new fields.StringField({ initial: "" }),
milieusocial: new fields.StringField({ initial: "" }),
poids: new fields.StringField({ initial: "" }),
cheveux: new fields.StringField({ initial: "" }),
sexe: new fields.StringField({ initial: "" }),
yeux: new fields.StringField({ initial: "" }),
enfance: new fields.StringField({ initial: "" }),
adulte: new fields.StringField({ initial: "" }),
loisirs: new fields.StringField({ initial: "" }),
singularite: new fields.StringField({ initial: "" }),
politique: new fields.StringField({ initial: "" }),
religion: new fields.StringField({ initial: "" }),
fantastique: new fields.StringField({ initial: "" }),
description: new fields.HTMLField({ initial: "" }),
notes: new fields.HTMLField({ initial: "" }),
gmnotes: new fields.HTMLField({ initial: "" })
}),
// Template core
subactors: new fields.ArrayField(new fields.StringField(), { initial: [] }),
lamesdestin: new fields.ArrayField(new fields.StringField(), { initial: [] }),
pointdestin: new fields.NumberField({ initial: 1, integer: true }),
fluide: new fields.NumberField({ initial: 5, integer: true }),
mpmb: new fields.NumberField({ initial: 0, integer: true }),
mpmn: new fields.NumberField({ initial: 0, integer: true }),
equipementlibre: new fields.HTMLField({ initial: "" }),
attributs: new fields.SchemaField({
constitution: new fields.SchemaField({
label: new fields.StringField({ initial: "Constitution" }),
abbrev: new fields.StringField({ initial: "constitution" }),
value: new fields.NumberField({ initial: 0, integer: true }),
hasmax: new fields.BooleanField({ initial: true }),
max: new fields.NumberField({ initial: 0, integer: true })
}),
physique: new fields.SchemaField({
label: new fields.StringField({ initial: "Aptitudes Physiques" }),
abbrev: new fields.StringField({ initial: "physique" }),
value: new fields.NumberField({ initial: 0, integer: true }),
hasmax: new fields.BooleanField({ initial: false }),
max: new fields.NumberField({ initial: 0, integer: true })
}),
culturegenerale: new fields.SchemaField({
label: new fields.StringField({ initial: "Culture Générale" }),
abbrev: new fields.StringField({ initial: "culturegenerale" }),
value: new fields.NumberField({ initial: 0, integer: true }),
hasmax: new fields.BooleanField({ initial: false }),
max: new fields.NumberField({ initial: 0, integer: true })
}),
habilite: new fields.SchemaField({
label: new fields.StringField({ initial: "Habilité" }),
abbrev: new fields.StringField({ initial: "habilite" }),
value: new fields.NumberField({ initial: 0, integer: true }),
hasmax: new fields.BooleanField({ initial: false }),
max: new fields.NumberField({ initial: 0, integer: true })
}),
perception: new fields.SchemaField({
label: new fields.StringField({ initial: "Perception" }),
abbrev: new fields.StringField({ initial: "perception" }),
value: new fields.NumberField({ initial: 0, integer: true }),
hasmax: new fields.BooleanField({ initial: false }),
max: new fields.NumberField({ initial: 0, integer: true })
}),
spiritualite: new fields.SchemaField({
label: new fields.StringField({ initial: "Spiritualite" }),
abbrev: new fields.StringField({ initial: "spiritualite" }),
value: new fields.NumberField({ initial: 0, integer: true }),
hasmax: new fields.BooleanField({ initial: false }),
iscard: new fields.BooleanField({ initial: true }),
max: new fields.NumberField({ initial: 0, integer: true })
}),
rationnalite: new fields.SchemaField({
label: new fields.StringField({ initial: "Rationnalite" }),
abbrev: new fields.StringField({ initial: "rationnalite" }),
value: new fields.NumberField({ initial: 0, integer: true }),
hasmax: new fields.BooleanField({ initial: false }),
iscard: new fields.BooleanField({ initial: true }),
max: new fields.NumberField({ initial: 0, integer: true })
})
})
};
}
}

12
modules/models/pnj.mjs Normal file
View File

@@ -0,0 +1,12 @@
/**
* Data model pour les PNJ (type "pnj")
*/
export default class PnjDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
npctype: new fields.StringField({ initial: "" }),
description: new fields.HTMLField({ initial: "" })
};
}
}

View File

@@ -0,0 +1,12 @@
/**
* Data model pour les sortilèges
*/
export default class SortilegeDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
seuil: new fields.NumberField({ initial: 0, integer: true }),
description: new fields.HTMLField({ initial: "" })
};
}
}

17
modules/models/tarot.mjs Normal file
View File

@@ -0,0 +1,17 @@
/**
* Data model pour les lames de tarot
*/
export default class TarotDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
tarottype: new fields.StringField({ initial: "" }),
numericvalueup: new fields.NumberField({ initial: 0, integer: true }),
numericvaluedown: new fields.NumberField({ initial: 0, integer: true }),
isdualside: new fields.BooleanField({ initial: false }),
ispositif: new fields.BooleanField({ initial: true }),
isgm: new fields.BooleanField({ initial: false }),
description: new fields.HTMLField({ initial: "" })
};
}
}

4982
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

17
package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "fvtt-malefices",
"version": "13.0.0",
"description": "Maléfices RPG for FoundryVTT - French",
"scripts": {
"build": "gulp build",
"watch": "gulp watch"
},
"author": "Uberwald/LeRatierBretonnien",
"license": "SEE LICENSE IN LICENCE.txt",
"devDependencies": {
"gulp": "^4.0.2",
"gulp-less": "^5.0.0",
"gulp-rename": "^2.0.0",
"gulp-sourcemaps": "^3.0.0"
}
}

Binary file not shown.

View File

View File

@@ -0,0 +1 @@
MANIFEST-000064

View File

View File

@@ -0,0 +1,7 @@
2026/02/27-10:28:01.892467 7f56f8bfd6c0 Recovering log #63
2026/02/27-10:28:01.948160 7f56f8bfd6c0 Delete type=0 #63
2026/02/27-10:28:01.948272 7f56f8bfd6c0 Delete type=3 #62
2026/02/27-14:29:37.861283 7f54e37ef6c0 Level-0 table #67: started
2026/02/27-14:29:37.861314 7f54e37ef6c0 Level-0 table #67: 0 bytes OK
2026/02/27-14:29:37.867488 7f54e37ef6c0 Delete type=0 #65
2026/02/27-14:29:37.880370 7f54e37ef6c0 Manual compaction at level-0 from '!items!2HWSdXDSFei9KC6y' @ 72057594037927935 : 1 .. '!items!xtYE2kVIfNtrXSoU' @ 0 : 0; will stop at (end)

View File

@@ -0,0 +1,3 @@
2026/02/27-00:10:01.008967 7fbc5bfff6c0 Recovering log #60
2026/02/27-00:10:01.061613 7fbc5bfff6c0 Delete type=3 #58
2026/02/27-00:10:01.061695 7fbc5bfff6c0 Delete type=0 #60

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

View File

@@ -0,0 +1 @@
MANIFEST-000064

View File

View File

@@ -0,0 +1,7 @@
2026/02/27-10:28:01.832073 7f56f93fe6c0 Recovering log #63
2026/02/27-10:28:01.886020 7f56f93fe6c0 Delete type=0 #63
2026/02/27-10:28:01.886089 7f56f93fe6c0 Delete type=3 #62
2026/02/27-14:29:37.854040 7f54e37ef6c0 Level-0 table #67: started
2026/02/27-14:29:37.854089 7f54e37ef6c0 Level-0 table #67: 0 bytes OK
2026/02/27-14:29:37.861166 7f54e37ef6c0 Delete type=0 #65
2026/02/27-14:29:37.880355 7f54e37ef6c0 Manual compaction at level-0 from '!items!5J6qIaWdnhEGMAXJ' @ 72057594037927935 : 1 .. '!items!nkRQU81L1gWOfaeo' @ 0 : 0; will stop at (end)

View File

@@ -0,0 +1,3 @@
2026/02/27-00:10:00.951941 7fbc5b7fe6c0 Recovering log #60
2026/02/27-00:10:01.006960 7fbc5b7fe6c0 Delete type=3 #58
2026/02/27-00:10:01.007022 7fbc5b7fe6c0 Delete type=0 #60

Binary file not shown.

View File

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