Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93f07adcee | |||
| eac8d93670 | |||
| 1cc6f92f15 | |||
| 0df4a5a9fb | |||
| d6b5891519 | |||
| a234ba5d14 | |||
| 463e9ebb19 | |||
| d8cfdfb96d | |||
| 1e63db8c39 | |||
| b97f8688e4 | |||
| 9836bd50e6 | |||
| 6b909b192b | |||
| e7a6c15bf7 | |||
| 1909ff443d | |||
| c72c9d8b02 | |||
| 536812e871 | |||
| 66fe1418f0 | |||
| 901df5b395 | |||
| f5d84832f3 | |||
| 216360e0d8 | |||
| a0502dc467 | |||
| c8b94b74a1 |
@@ -0,0 +1,112 @@
|
||||
name: Release Creation
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "💡 The ${{ gitea.repository }} repository will cloned to the runner."
|
||||
|
||||
#- uses: actions/checkout@v3
|
||||
- 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-mournblade/releases/download/latest/system.json
|
||||
download: https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{github.event.release.tag_name}}/fvtt-mournblade.zip
|
||||
|
||||
# Set up Node.js and build compendium packs from source
|
||||
- name: Setup Node.js
|
||||
uses: https://github.com/actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Build journal-aide compendium pack
|
||||
run: |
|
||||
rm -rf packs/journal-aide/journal-aide
|
||||
./node_modules/.bin/fvtt package pack -n journal-aide --inputDirectory ./packs/src/journal-aide --outputDirectory ./packs/journal-aide
|
||||
|
||||
- name: Build pouvoirs-elementaires compendium pack
|
||||
run: |
|
||||
rm -rf packs/pouvoirs-elementaires/pouvoirs-elementaires
|
||||
./node_modules/.bin/fvtt package pack -n pouvoirs-elementaires --inputDirectory ./packs/src/pouvoirs-elementaires --outputDirectory ./packs/pouvoirs-elementaires
|
||||
|
||||
- name: Build creatures-elementaires compendium pack
|
||||
run: |
|
||||
rm -rf packs/creatures-elementaires/creatures-elementaires
|
||||
./node_modules/.bin/fvtt package pack -n creatures-elementaires --inputDirectory ./packs/src/creatures-elementaires --outputDirectory ./packs/creatures-elementaires
|
||||
|
||||
- name: Build automatons compendium pack
|
||||
run: |
|
||||
rm -rf packs/automatons/automatons
|
||||
./node_modules/.bin/fvtt package pack -n automatons --inputDirectory ./packs/src/automatons --outputDirectory ./packs/automatons
|
||||
|
||||
- name: Build capacites-automaton compendium pack
|
||||
run: |
|
||||
rm -rf packs/capacites-automaton/capacites-automaton
|
||||
./node_modules/.bin/fvtt package pack -n capacites-automaton --inputDirectory ./packs/src/capacites-automaton --outputDirectory ./packs/capacites-automaton
|
||||
|
||||
- name: Build traits-demoniaques compendium pack
|
||||
run: |
|
||||
rm -rf packs/traits-demoniaques/traits-demoniaques
|
||||
./node_modules/.bin/fvtt package pack -n traits-demoniaques --inputDirectory ./packs/src/traits-demoniaques --outputDirectory ./packs/traits-demoniaques
|
||||
|
||||
- name: Build faiblesses-demoniaques compendium pack
|
||||
run: |
|
||||
rm -rf packs/faiblesses-demoniaques/faiblesses-demoniaques
|
||||
./node_modules/.bin/fvtt package pack -n faiblesses-demoniaques --inputDirectory ./packs/src/faiblesses-demoniaques --outputDirectory ./packs/faiblesses-demoniaques
|
||||
|
||||
- name: Build demons-types compendium pack
|
||||
run: |
|
||||
rm -rf packs/demons-types/demons-types
|
||||
./node_modules/.bin/fvtt package pack -n demons-types --inputDirectory ./packs/src/demons --outputDirectory ./packs/demons-types
|
||||
|
||||
# Create a zip file with all files required by the module to add to the release
|
||||
- run: |
|
||||
apt update -y
|
||||
apt install -y zip
|
||||
|
||||
- run: zip -r ./fvtt-mournblade.zip system.json README.md LICENSE assets/ lang/ modules/ packs/ styles/ templates/ template.json
|
||||
|
||||
- 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-mournblade.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-mournblade'
|
||||
version: ${{github.event.release.tag_name}}
|
||||
manifest: 'https://www.uberwald.me/gitea/public/fvtt-mournblade/releases/download/latest/system.json'
|
||||
notes: 'https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{github.event.release.tag_name}}/fvtt-mournblade.zip'
|
||||
compatibility-minimum: '13'
|
||||
compatibility-verified: '14'
|
||||
@@ -1 +1,4 @@
|
||||
.history/
|
||||
node_modules
|
||||
.github/
|
||||
regles/
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 0h512v512H0z" fill="#661f1f" fill-opacity="1"></path><g class="" style="" transform="translate(0,0)"><path d="M94.055 21.9 18.998 96.96l42.727 23.6-26.98 26.952L142.35 212.39c-40.443 70.148-30.72 161.07 29.2 220.958 71.605 71.606 187.737 71.587 259.356 0 71.62-71.587 71.642-187.654.037-259.22-59.915-59.878-150.896-69.57-221.084-29.177L144.95 37.415l-8.44 8.432-18.588 18.57L94.055 21.9zm47.224 45.598 62.337 103.275 8.098-5.248c44.21-28.663 99.014-34.044 147.166-16.078-1.16-.026-2.328-.04-3.503-.04-38.988 0-70.594 14.807-70.594 33.073 0 18.27 31.606 33.075 70.594 33.075 31.53 0 58.225-9.684 67.287-23.05 15.942 17.34 27.492 37.224 34.65 58.253-7.76-3.387-18.28-6.706-30.902-9.563-31.383-7.1-75.547-11.615-124.305-11.615-48.757 0-92.92 4.514-124.304 11.615-13.71 3.102-24.997 6.75-32.893 10.438a163.85 163.85 0 0 1 18.018-37.383l5.263-8.104-103.33-62.3 13.894-13.88 46.937 25.923 27.914-27.915-26.18-46.635 13.855-13.842zm-1.087 201.287c.482.28.982.56 1.506.84 7.89 4.22 20.41 8.487 36.103 12.037 31.383 7.1 75.547 11.615 124.304 11.615 48.758 0 92.922-4.514 124.305-11.615 15.687-3.55 28.203-7.813 36.094-12.033a164.248 164.248 0 0 1 2.746 17.643c-9.432 4.277-21.204 7.893-35.074 11.032-33.205 7.513-78.27 12.037-128.07 12.037-49.802 0-94.866-4.524-128.07-12.037-14.67-3.32-27-7.17-36.69-11.776a164.503 164.503 0 0 1 2.845-17.745z" fill="#fff" fill-opacity="1"></path></g></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
@@ -0,0 +1,35 @@
|
||||
const gulp = require('gulp');
|
||||
const less = require('gulp-less');
|
||||
const sourcemaps = require('gulp-sourcemaps');
|
||||
|
||||
// Paths
|
||||
const paths = {
|
||||
styles: {
|
||||
src: 'less/**/*.less',
|
||||
dest: 'styles/'
|
||||
}
|
||||
};
|
||||
|
||||
// Compile LESS to CSS
|
||||
function styles() {
|
||||
return gulp.src('less/mournblade.less')
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(less())
|
||||
.pipe(sourcemaps.write('.'))
|
||||
.pipe(gulp.dest(paths.styles.dest));
|
||||
}
|
||||
|
||||
// Watch files
|
||||
function watchFiles() {
|
||||
gulp.watch(paths.styles.src, styles);
|
||||
}
|
||||
|
||||
// Define complex tasks
|
||||
const build = gulp.series(styles);
|
||||
const watch = gulp.series(build, watchFiles);
|
||||
|
||||
// Export tasks
|
||||
exports.styles = styles;
|
||||
exports.build = build;
|
||||
exports.watch = watch;
|
||||
exports.default = build;
|
||||
@@ -43,6 +43,7 @@
|
||||
"MNBL.disarm": "Disarm",
|
||||
"MNBL.doubleD20": "Double d20 (1 Shard Point)",
|
||||
"MNBL.dramaticfailure": "Dramatic Failure",
|
||||
"MNBL.oddresult": "Odd result — the die counts as 0",
|
||||
"MNBL.duration": "Duration",
|
||||
"MNBL.easy": "Easy (5)",
|
||||
"MNBL.eclat": "Shard",
|
||||
@@ -169,6 +170,7 @@
|
||||
"MNBL.weaponscapacities": "Weapons/Abilities",
|
||||
"MNBL.weapontype": "Weapon Type",
|
||||
"MNBL.weight": "Weight",
|
||||
"MNBL.total": "Total",
|
||||
"Présence": "Presence",
|
||||
"Puissance": "Might",
|
||||
"Trempe": "Mettle",
|
||||
@@ -197,5 +199,125 @@
|
||||
"traitchaotique": "Background",
|
||||
"traitespece": "Species Trait"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MNBL.potion": "Potion",
|
||||
"MNBL.potionRune": "Rune",
|
||||
"MNBL.potionSeuil": "Rune Threshold",
|
||||
"MNBL.potionPointsAme": "Soul Points invested",
|
||||
"MNBL.potionForme": "Form",
|
||||
"MNBL.potionStatut": "Status",
|
||||
"MNBL.potionVirulence": "Virulence",
|
||||
"MNBL.potionDuree": "Duration",
|
||||
"MNBL.potionConservation": "Conservation",
|
||||
"MNBL.potionTemps": "Preparation Time",
|
||||
"MNBL.potionEffetCuratif": "Curative Effect",
|
||||
"MNBL.potionEffetLetal": "Lethal Effect",
|
||||
"MNBL.potionEffetSecondaire": "Side Effects",
|
||||
"MNBL.potionDescription": "Description",
|
||||
"MNBL.potionLiquide": "Liquid",
|
||||
"MNBL.potionOnguent": "Ointment",
|
||||
"MNBL.potionCachets": "Tablets",
|
||||
"MNBL.potionPilules": "Pills",
|
||||
"MNBL.potionInconnue": "Unknown",
|
||||
"MNBL.potionEfficace": "Effective",
|
||||
"MNBL.potionHeroique": "Heroic",
|
||||
"MNBL.potionInefficace": "Ineffective",
|
||||
"MNBL.potionPoison": "Poison",
|
||||
"MNBL.potions": "Potions",
|
||||
"MNBL.potionStatut": "Status",
|
||||
"MNBL.potionDuree": "Duration",
|
||||
"MNBL.preparePotion": "Prepare a Potion",
|
||||
"MNBL.capacites": "Capacities / Powers",
|
||||
"MNBL.capacite": "Capacity",
|
||||
"MNBL.typeCapacite": "Capacity Type",
|
||||
"MNBL.typeCapaciteElue": "Chosen Power",
|
||||
"MNBL.typeCapaciteElementaire": "Elemental Power",
|
||||
"MNBL.typeCapaciteDemoniaque": "Demonic Power",
|
||||
"MNBL.typeCapaciteAutomaton": "Automaton Ability",
|
||||
"MNBL.typeCapaciteCreature": "Creature Power",
|
||||
"MNBL.typeCapaciteFaiblesseDemoniaque": "Demonic Weakness",
|
||||
"MNBL.creatureTypeCreature": "Creature",
|
||||
"MNBL.creatureTypeDemon": "Demon",
|
||||
"MNBL.creatureTypeElementaire": "Elemental",
|
||||
"MNBL.elementTypeAir": "Air",
|
||||
"MNBL.elementTypeTerre": "Earth",
|
||||
"MNBL.elementTypeFeu": "Fire",
|
||||
"MNBL.elementTypeEau": "Water",
|
||||
"MNBL.creatureType": "Creature Type",
|
||||
"MNBL.elementType": "Element",
|
||||
"MNBL.demonType": "Demon Type",
|
||||
"MNBL.demonPuissance": "Power Level",
|
||||
"MNBL.demonTypeNone": "— Undefined —",
|
||||
"MNBL.demonTypeCombat": "Combat Demon",
|
||||
"MNBL.demonTypeDesir": "Desire Demon",
|
||||
"MNBL.demonTypeSavoir": "Knowledge Demon",
|
||||
"MNBL.demonTypeProtection": "Protection Demon",
|
||||
"MNBL.demonTypeVoyage": "Travel Demon",
|
||||
"MNBL.demonPuissanceNone": "— Undefined —",
|
||||
"MNBL.demonPuissanceMineur": "Minor",
|
||||
"MNBL.demonPuissanceMedian": "Median",
|
||||
"MNBL.demonPuissanceMajeur": "Major",
|
||||
"MNBL.creatureTypeAutomaton": "Automaton",
|
||||
"MNBL.automatonType": "Automaton Type",
|
||||
"MNBL.automatonPuissance": "Power Level",
|
||||
"MNBL.automatonTypeNone": "— Undefined —",
|
||||
"MNBL.automatonTypeCombat": "Combat",
|
||||
"MNBL.automatonTypeVoyage": "Travel",
|
||||
"MNBL.automatonTypePerception": "Perception",
|
||||
"MNBL.automatonTypeRestauration": "Restoration",
|
||||
"MNBL.automatonTypeReparateur": "Repairer",
|
||||
"MNBL.automatonPuissanceNone": "— Undefined —",
|
||||
"MNBL.automatonPuissanceMineur": "Minor",
|
||||
"MNBL.automatonPuissanceMedian": "Median",
|
||||
"MNBL.automatonPuissanceMajeur": "Major",
|
||||
"MNBL.automatonVoyageType": "Travel Type",
|
||||
"MNBL.automatonVoyageTypeNone": "— Undefined —",
|
||||
"MNBL.automatonVoyageTypeTerrestre": "Terrestrial",
|
||||
"MNBL.automatonVoyageTypeAquatique": "Aquatic",
|
||||
"MNBL.automatonVoyageTypeAerien": "Aerial",
|
||||
"MNBL.automatonVoyageTypeExtradimensionnel": "Extra-dimensional",
|
||||
"MNBL.invoquerElementaire": "Summon an Elemental",
|
||||
"MNBL.bannirElementaire": "Banish",
|
||||
"MNBL.invocationsActives": "Active Invocations",
|
||||
"MNBL.invocationElement": "Element",
|
||||
"MNBL.invocationTier": "Power",
|
||||
"MNBL.invocationSeuil": "Threshold",
|
||||
"MNBL.invocationAmeExtra": "Extra Soul Points",
|
||||
"MNBL.invocationAmeTotal": "Total Soul Cost",
|
||||
"MNBL.invocationTemps": "Concentration Time",
|
||||
"MNBL.invocationBonusPacte": "Elemental Pact Bonus",
|
||||
"MNBL.invocationHautParler": "Savoir : Haut-Parler",
|
||||
"MNBL.invocationSeigneursElem": "Savoir : Seigneurs Élémentaires",
|
||||
"MNBL.invocationResultSucces": "The Elemental is summoned!",
|
||||
"MNBL.invocationResultHeroique": "Heroic success! You may freely increase one of the Elemental's attributes by +2.",
|
||||
"MNBL.invocationResultEchec": "The invocation failed. You lose half the Soul Points spent.",
|
||||
"MNBL.invocationResultDramatique": "Dramatic failure! The invocation failed and you lose all invested Soul Points. A natural catastrophe is unleashed!",
|
||||
"MNBL.invocationTierMineur": "Minor (Threshold 15)",
|
||||
"MNBL.invocationTierMedian": "Median (Threshold 20)",
|
||||
"MNBL.invocationTierMajeur": "Major (Threshold 25)",
|
||||
"MNBL.invocationConcentrationMineur": "1 round",
|
||||
"MNBL.invocationConcentrationMedian": "1 minute",
|
||||
"MNBL.invocationConcentrationMajeur": "1 hour",
|
||||
"MNBL.invocationAmeBloque": "Blocked Soul",
|
||||
|
||||
"MNBL.invoquerDemon": "Invoke a Demon",
|
||||
"MNBL.invoquerEsprit": "Invoke a Law Spirit",
|
||||
"MNBL.libererDemon": "Release",
|
||||
"MNBL.invocationsDemons": "Active Demonic Invocations",
|
||||
"MNBL.invocationDemonSeuil": "Difficulty Threshold",
|
||||
"MNBL.invocationDemonAme": "Soul Spent",
|
||||
"MNBL.invocationDemonCoercition": "Coercion",
|
||||
"MNBL.invocationDemonLoiChaos": "Lore: Law & Chaos",
|
||||
"MNBL.invocationDemonResultSucces": "The Demon is invoked!",
|
||||
"MNBL.invocationDemonResultHeroique": "Heroic success! More favorable agreement.",
|
||||
"MNBL.invocationDemonResultEchec": "Invocation failed. You lose half the Soul points spent.",
|
||||
"MNBL.invocationDemonResultDramatique": "Dramatic failure! All Soul points are lost.",
|
||||
|
||||
"MNBL.enchantementLoi": "Law Enchantment",
|
||||
"MNBL.enchanter": "Enchant",
|
||||
"MNBL.enchantementActif": "Enchanted",
|
||||
"MNBL.enchantementBonus": "Bonus",
|
||||
"MNBL.enchantementAntiChaos": "Anti-Chaos",
|
||||
"MNBL.enchantementStandard": "Standard (skill bonus)",
|
||||
"MNBL.enchantementTypeAntiChaos": "Anti-Chaos (luminous aura)"
|
||||
}
|
||||
@@ -22,7 +22,8 @@
|
||||
"runeeffect": "Effet de Rune",
|
||||
"bouclier": "Bouclier",
|
||||
"modifier": "Modificateur",
|
||||
"traitespece": "Trait d'Espèce"
|
||||
"traitespece": "Trait d'Espèce",
|
||||
"potion": "Potion"
|
||||
}
|
||||
},
|
||||
"Adresse": "Adresse",
|
||||
@@ -30,7 +31,6 @@
|
||||
"Clairvoyance": "Clairvoyance",
|
||||
"Présence": "Présence",
|
||||
"Trempe": "Trempe",
|
||||
|
||||
"MNBL.assaut": "Assaut",
|
||||
"MNBL.preciseattack": "Attaque Précise",
|
||||
"MNBL.feint": "Feinte",
|
||||
@@ -38,7 +38,6 @@
|
||||
"MNBL.charge": "Charger",
|
||||
"MNBL.contain": "Contenir l'adversaire",
|
||||
"MNBL.disarm": "Désarmer",
|
||||
|
||||
"MNBL.none": "Aucun",
|
||||
"MNBL.lightcover": "Rondache ou léger (-2)",
|
||||
"MNBL.mediumcover": "Pavois ou à moitié (-5)",
|
||||
@@ -46,12 +45,10 @@
|
||||
"MNBL.roll": "Jet",
|
||||
"MNBL.defensecapacity": "Capacité défensive",
|
||||
"MNBL.attackcapacity": "Capacité offensive",
|
||||
|
||||
"MNBL.lessthanshort": "Moins que courte (10)",
|
||||
"MNBL.shortmore": "Courte et + (10)",
|
||||
"MNBL.mediummore": "Moyenne et + (20)",
|
||||
"MNBL.longmore": "Longue et + (25)",
|
||||
|
||||
"MNBL.noneunknwon": "Aucune/Inconnue",
|
||||
"MNBL.easy": "Facile (5)",
|
||||
"MNBL.medium": "Moyenne (10)",
|
||||
@@ -59,32 +56,28 @@
|
||||
"MNBL.hazardous": "Hasardeuse (20)",
|
||||
"MNBL.insane": "Insensée (25)",
|
||||
"MNBL.puremadness": "Pure Folie (30)",
|
||||
|
||||
"MNBL.pronouncerune": "Prononcer la rune",
|
||||
"MNBL.tracerune": "Tracer la rune",
|
||||
"MNBL.pronounced": "Prononcée",
|
||||
"MNBL.traced": "Tracée",
|
||||
|
||||
"MNBL.meleeweapon": "Arme de contact",
|
||||
"MNBL.meleethrowweapon": "Arme de contact et de Jet",
|
||||
"MNBL.throwweapon": "Arme de Lancer",
|
||||
"MNBL.shootweapon": "Arme de Tir",
|
||||
"MNBL.specialweapon": "Spécial (capacité/don)",
|
||||
|
||||
"MNBL.all": "Tous",
|
||||
"MNBL.beastslords": "Seigneurs des Bêtes",
|
||||
"MNBL.elementslords": "Seigneurs Elementaires",
|
||||
|
||||
"MNBL.law": "Loi",
|
||||
"MNBL.chaos": "Chaos",
|
||||
"MNBL.level": "Niveau",
|
||||
"MNBL.points": "Points",
|
||||
"MNBL.aspect": "Aspect",
|
||||
"MNBL.margin": "Marge",
|
||||
"MNBL.goodadventure" : "Bonne Aventure",
|
||||
"MNBL.goodadventure": "Bonne Aventure",
|
||||
"MNBL.base": "Base",
|
||||
"MNBL.current": "Actuelle",
|
||||
"MNBL.alignement" : "Alignement",
|
||||
"MNBL.alignement": "Alignement",
|
||||
"MNBL.eclat": "Eclat",
|
||||
"MNBL.exp": "Expérience",
|
||||
"MNBL.attributes": "Attributs",
|
||||
@@ -97,7 +90,7 @@
|
||||
"MNBL.malus": "Malus",
|
||||
"MNBL.nonlethal": "Non Létaux",
|
||||
"MNBL.lethal": "Létaux",
|
||||
"MNBL.automalus" : "Malus Auto",
|
||||
"MNBL.automalus": "Malus Auto",
|
||||
"MNBL.soul": "Ame",
|
||||
"MNBL.currentmax": "Max Actuel",
|
||||
"MNBL.consumed": "Consommé",
|
||||
@@ -141,11 +134,10 @@
|
||||
"MNBL.hair": "Cheveux",
|
||||
"MNBL.eyes": "Yeux",
|
||||
"MNBL.preferredhand": "Main Préférée",
|
||||
"MNBL.weight": "Weight",
|
||||
"MNBL.weight": "Poids",
|
||||
"MNBL.soulmultiplier": "Multiplicateur d'âme",
|
||||
"MNBL.ignorehealthmalus": "Ignore le malus de santé",
|
||||
"MNBL.ignoresoulmalus": "Ignore le malus d'âme",
|
||||
|
||||
"MNBL.weapon": "Arme",
|
||||
"MNBL.nextattackbonus": "Bonus pour prochaine attaque",
|
||||
"MNBL.nextactionmalus": "Malus au défenseur pour prochaine action",
|
||||
@@ -156,12 +148,12 @@
|
||||
"MNBL.usedpredilection": "Prédilection utilisée",
|
||||
"MNBL.soulpoints": "Points d'âme",
|
||||
"MNBL.formula": "Formule",
|
||||
"MNBL.dice":"Dé",
|
||||
"MNBL.dice": "Dé",
|
||||
"MNBL.success": "Succés",
|
||||
"MNBL.failure": "Echec",
|
||||
"MNBL.heroicsuccess": "Succés Héroïque",
|
||||
"MNBL.dramaticfailure": "Echec Dramatique",
|
||||
|
||||
"MNBL.oddresult": "Résultat impair — le dé compte pour 0",
|
||||
"MNBL.attackmountbonus": "Attaquant monté vs def. au sol (+5)",
|
||||
"MNBL.targetdefense": "Défense adversaire",
|
||||
"MNBL.shootmodifier": "Modificateurs de Tir",
|
||||
@@ -181,7 +173,6 @@
|
||||
"MNBL.soulmalus": "Malus d'âme",
|
||||
"MNBL.registeredmodifiers": "Modificateurs enregistrés",
|
||||
"MNBL.doubleD20": "Doubler le d20 (1 Point d'Eclat)",
|
||||
|
||||
"MNBL.pronouncedrune": "Rune prononcée",
|
||||
"MNBL.tracedrune": "Rune tracée",
|
||||
"MNBL.equipped": "Equipé",
|
||||
@@ -202,17 +193,141 @@
|
||||
"MNBL.ignorearmor": "Ignore l'armure",
|
||||
"MNBL.creatureresourcecost": "Cout en Ressources (créatures)",
|
||||
"MNBL.shortrange": "Portée courte",
|
||||
"MNBL.mediumrange":"Portée moyenne",
|
||||
"MNBL.longrange":"Portée longue",
|
||||
"MNBL.mediumrange": "Portée moyenne",
|
||||
"MNBL.longrange": "Portée longue",
|
||||
"MNBL.reloadduration": "Temps de rechargement",
|
||||
"MNBL.attacks": "Attaques",
|
||||
"MNBL.ressources": "Ressources",
|
||||
"MNBL.weaponscapacities": "Armes/Capacités",
|
||||
"MNBL.use": "Utiliser",
|
||||
"MNBL.speciestrait": "Trait d'espèce",
|
||||
|
||||
"MNBL.attribute": "Attribut",
|
||||
"MNBL.Protections": "Protections",
|
||||
"MNBL.rune": "Rune"
|
||||
"MNBL.rune": "Rune",
|
||||
"MNBL.total": "Total",
|
||||
"MNBL.description": "Description",
|
||||
"MNBL.details": "Détails",
|
||||
"MNBL.sacrifice": "Sacrifice",
|
||||
"MNBL.identity": "Identité",
|
||||
"MNBL.gmtools": "Outils MJ",
|
||||
"MNBL.potion": "Potion",
|
||||
"MNBL.potionRune": "Rune",
|
||||
"MNBL.potionSeuil": "Seuil de la Rune",
|
||||
"MNBL.potionPointsAme": "Points d'Âme investis",
|
||||
"MNBL.potionForme": "Forme",
|
||||
"MNBL.potionStatut": "Statut",
|
||||
"MNBL.potionVirulence": "Virulence",
|
||||
"MNBL.potionDuree": "Durée d'effet",
|
||||
"MNBL.potionConservation": "Conservation",
|
||||
"MNBL.potionTemps": "Temps de préparation",
|
||||
"MNBL.potionEffetCuratif": "Effet curatif",
|
||||
"MNBL.potionEffetLetal": "Effet létal",
|
||||
"MNBL.potionEffetSecondaire": "Effets secondaires",
|
||||
"MNBL.potionDescription": "Description",
|
||||
"MNBL.potionLiquide": "Liquide",
|
||||
"MNBL.potionOnguent": "Onguent",
|
||||
"MNBL.potionCachets": "Cachets",
|
||||
"MNBL.potionPilules": "Pilules",
|
||||
"MNBL.potionInconnue": "Inconnue",
|
||||
"MNBL.potionEfficace": "Efficace",
|
||||
"MNBL.potionHeroique": "Héroïque",
|
||||
"MNBL.potionInefficace": "Inefficace",
|
||||
"MNBL.potionPoison": "Poison",
|
||||
"MNBL.potions": "Potions",
|
||||
"MNBL.potionStatut": "Statut",
|
||||
"MNBL.potionDuree": "Durée",
|
||||
"MNBL.preparePotion": "Préparer une Potion",
|
||||
"MNBL.capacites": "Capacités / Pouvoirs",
|
||||
"MNBL.capacite": "Capacité",
|
||||
"MNBL.typeCapacite": "Type de capacité",
|
||||
"MNBL.typeCapaciteElue": "Pouvoir Élue",
|
||||
"MNBL.typeCapaciteElementaire": "Pouvoir Élémentaire",
|
||||
"MNBL.typeCapaciteDemoniaque": "Pouvoir Démoniaque",
|
||||
"MNBL.typeCapaciteAutomaton": "Capacité Automaton",
|
||||
"MNBL.typeCapaciteCreature": "Pouvoir Créature",
|
||||
"MNBL.typeCapaciteFaiblesseDemoniaque": "Faiblesse Démoniaque",
|
||||
"MNBL.creatureTypeCreature": "Créature",
|
||||
"MNBL.creatureTypeDemon": "Démon",
|
||||
"MNBL.creatureTypeElementaire": "Élémentaire",
|
||||
"MNBL.elementTypeAir": "Air",
|
||||
"MNBL.elementTypeTerre": "Terre",
|
||||
"MNBL.elementTypeFeu": "Feu",
|
||||
"MNBL.elementTypeEau": "Eau",
|
||||
"MNBL.creatureType": "Type de créature",
|
||||
"MNBL.elementType": "Élément",
|
||||
"MNBL.demonType": "Type de Démon",
|
||||
"MNBL.demonPuissance": "Puissance",
|
||||
"MNBL.demonTypeNone": "— Non défini —",
|
||||
"MNBL.demonTypeCombat": "Démon de Combat",
|
||||
"MNBL.demonTypeDesir": "Démon du Désir",
|
||||
"MNBL.demonTypeSavoir": "Démon du Savoir",
|
||||
"MNBL.demonTypeProtection": "Démon de Protection",
|
||||
"MNBL.demonTypeVoyage": "Démon du Voyage",
|
||||
"MNBL.demonPuissanceNone": "— Non défini —",
|
||||
"MNBL.demonPuissanceMineur": "Mineur",
|
||||
"MNBL.demonPuissanceMedian": "Médian",
|
||||
"MNBL.demonPuissanceMajeur": "Majeur",
|
||||
"MNBL.creatureTypeAutomaton": "Automaton",
|
||||
"MNBL.automatonType": "Type d'Automaton",
|
||||
"MNBL.automatonPuissance": "Puissance",
|
||||
"MNBL.automatonTypeNone": "— Non défini —",
|
||||
"MNBL.automatonTypeCombat": "Combat",
|
||||
"MNBL.automatonTypeVoyage": "Voyage",
|
||||
"MNBL.automatonTypePerception": "Perception",
|
||||
"MNBL.automatonTypeRestauration": "Restauration",
|
||||
"MNBL.automatonTypeReparateur": "Réparateur",
|
||||
"MNBL.automatonPuissanceNone": "— Non défini —",
|
||||
"MNBL.automatonPuissanceMineur": "Mineur",
|
||||
"MNBL.automatonPuissanceMedian": "Médian",
|
||||
"MNBL.automatonPuissanceMajeur": "Majeur",
|
||||
"MNBL.automatonVoyageType": "Type de voyage",
|
||||
"MNBL.automatonVoyageTypeNone": "— Non défini —",
|
||||
"MNBL.automatonVoyageTypeTerrestre": "Terrestre",
|
||||
"MNBL.automatonVoyageTypeAquatique": "Aquatique",
|
||||
"MNBL.automatonVoyageTypeAerien": "Aérien",
|
||||
"MNBL.automatonVoyageTypeExtradimensionnel": "Extra-dimensionnel",
|
||||
"MNBL.invoquerElementaire": "Invoquer un Élémentaire",
|
||||
"MNBL.bannirElementaire": "Bannir",
|
||||
"MNBL.invocationsActives": "Invocations actives",
|
||||
"MNBL.invocationElement": "Élément",
|
||||
"MNBL.invocationTier": "Puissance",
|
||||
"MNBL.invocationSeuil": "Seuil",
|
||||
"MNBL.invocationAmeExtra": "Points d'Âme supplémentaires",
|
||||
"MNBL.invocationAmeTotal": "Coût total en Âme",
|
||||
"MNBL.invocationTemps": "Temps de concentration",
|
||||
"MNBL.invocationBonusPacte": "Bonus Pacte élémentaire",
|
||||
"MNBL.invocationHautParler": "Savoir : Haut-Parler",
|
||||
"MNBL.invocationSeigneursElem": "Savoir : Seigneurs Élémentaires",
|
||||
"MNBL.invocationResultSucces": "L'Élémentaire est invoqué !",
|
||||
"MNBL.invocationResultHeroique": "Réussite héroïque ! Vous pouvez majorer gratuitement de +2 un attribut de l'Élémentaire.",
|
||||
"MNBL.invocationResultEchec": "L'invocation a échoué. Vous perdez la moitié des points d'Âme dépensés.",
|
||||
"MNBL.invocationResultDramatique": "Échec dramatique ! L'invocation a échoué et vous perdez tous les points d'Âme investis. Une catastrophe naturelle se déclenche !",
|
||||
"MNBL.invocationTierMineur": "Mineur (Seuil 15)",
|
||||
"MNBL.invocationTierMedian": "Médian (Seuil 20)",
|
||||
"MNBL.invocationTierMajeur": "Majeur (Seuil 25)",
|
||||
"MNBL.invocationConcentrationMineur": "1 tour",
|
||||
"MNBL.invocationConcentrationMedian": "1 minute",
|
||||
"MNBL.invocationConcentrationMajeur": "1 heure",
|
||||
"MNBL.invocationAmeBloque": "Âme bloquée",
|
||||
|
||||
"MNBL.invoquerDemon": "Invoquer un Démon",
|
||||
"MNBL.invoquerEsprit": "Invoquer un Esprit de la Loi",
|
||||
"MNBL.libererDemon": "Libérer",
|
||||
"MNBL.invocationsDemons": "Invocations démoniaques actives",
|
||||
"MNBL.invocationDemonSeuil": "Seuil de difficulté",
|
||||
"MNBL.invocationDemonAme": "Âme dépensée",
|
||||
"MNBL.invocationDemonCoercition": "Coercition",
|
||||
"MNBL.invocationDemonLoiChaos": "Savoir : Loi & Chaos",
|
||||
"MNBL.invocationDemonResultSucces": "Le Démon est invoqué !",
|
||||
"MNBL.invocationDemonResultHeroique": "Réussite héroïque ! Accord plus favorable.",
|
||||
"MNBL.invocationDemonResultEchec": "L'invocation a échoué. Vous perdez la moitié des points d'Âme dépensés.",
|
||||
"MNBL.invocationDemonResultDramatique": "Échec dramatique ! Tous les points d'Âme sont perdus.",
|
||||
|
||||
"MNBL.enchantementLoi": "Enchantement de la Loi",
|
||||
"MNBL.enchanter": "Enchanter",
|
||||
"MNBL.enchantementActif": "Enchanté",
|
||||
"MNBL.enchantementBonus": "Bonus",
|
||||
"MNBL.enchantementAntiChaos": "Anti-Chaos",
|
||||
"MNBL.enchantementStandard": "Standard (bonus de compétence)",
|
||||
"MNBL.enchantementTypeAntiChaos": "Anti-Chaos (aura lumineuse)"
|
||||
}
|
||||
@@ -0,0 +1,950 @@
|
||||
/* ==================== Actor Sheet Styles ==================== */
|
||||
|
||||
// NOTE: Ce fichier surcharge certaines règles de simple-converted.less
|
||||
// Les sélecteurs .fvtt-mournblade.actor sont plus spécifiques et prennent priorité
|
||||
|
||||
.fvtt-mournblade.actor {
|
||||
// Background pour toute la fiche d'acteur
|
||||
background: url("../assets/ui/pc_sheet_bg.webp") repeat;
|
||||
|
||||
// AppV2 - Structure flex pour permettre le scroll
|
||||
.window-content {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Main form structure
|
||||
form,
|
||||
.sheet-form-layout {
|
||||
height: 100%;
|
||||
background: url("../assets/ui/pc_sheet_bg.webp") repeat-y;
|
||||
color: black;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
// La section racine du template (if nested inside form, or simple direct children handling)
|
||||
> section {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// SURCHARGE: simple-converted.less définit flex: 0 0 210px
|
||||
.sheet-header {
|
||||
background: url("../assets/ui/pc_sheet_bg.webp") repeat;
|
||||
padding: 0.5rem;
|
||||
margin: 0;
|
||||
flex: 0 0 auto !important; // Override simple-converted
|
||||
overflow: visible !important; // Override simple-converted
|
||||
|
||||
.background-sheet-header {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.header-main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.header-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
border: 2px solid rgba(139, 69, 19, 0.5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stat-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
padding: 0.35rem 0.6rem;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(139, 69, 19, 0.4);
|
||||
border-radius: 3px;
|
||||
|
||||
&.stat-group-health {
|
||||
border-left: 3px solid rgba(200, 0, 0, 0.6);
|
||||
background: rgba(40, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
color: #f5e6d3;
|
||||
margin: 0;
|
||||
padding: 0 0 0.3rem 0;
|
||||
border-bottom: 1px solid rgba(139, 69, 19, 0.5);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-family: "CentaurMT", serif;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.stat-values {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.7rem;
|
||||
color: #d4c5b0;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 0.85rem;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border: 1px solid rgba(139, 69, 19, 0.6);
|
||||
border-radius: 3px;
|
||||
min-width: 2.5rem;
|
||||
text-align: center;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.9);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stat-input {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
padding: 0.25rem 0.4rem;
|
||||
background: rgba(40, 30, 20, 0.7);
|
||||
border: 1px solid rgba(139, 69, 19, 0.7);
|
||||
color: #fff;
|
||||
border-radius: 3px;
|
||||
text-align: center;
|
||||
min-width: 2.5rem;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.9);
|
||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
|
||||
&.stat-input-single {
|
||||
min-width: 3.5rem;
|
||||
}
|
||||
|
||||
&.stat-input-damage {
|
||||
border-color: rgba(200, 0, 0, 0.8);
|
||||
background: rgba(80, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(60, 45, 30, 0.8);
|
||||
border-color: rgba(255, 102, 0, 0.6);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: rgba(255, 102, 0, 1);
|
||||
box-shadow: 0 0 6px rgba(255, 102, 0, 0.5), inset 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
background: rgba(70, 50, 35, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.header-fields {
|
||||
h4.item-name-label.competence-name {
|
||||
font-size: 0.75rem;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.item-name-label.competence-name {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
label.item-name-label.competence-name {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.item-field-label-short {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.status-small-label {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sheet navigation tabs
|
||||
// SURCHARGE: simple-converted.less définit flex: 0
|
||||
.sheet-tabs {
|
||||
margin: 0;
|
||||
padding: 0 0.5rem;
|
||||
flex: 0 0 auto !important; // Override simple-converted
|
||||
}
|
||||
|
||||
// Sheet body - section scrollable
|
||||
// SURCHARGE CRITIQUE: simple-converted.less définit height: auto sur .sheet-body et .tab
|
||||
// Ces surcharges permettent le scroll vertical
|
||||
.sheet-body {
|
||||
margin: 0;
|
||||
padding: 0.5rem;
|
||||
flex: 1 !important; // Override simple-converted
|
||||
min-height: 0 !important; // Critique pour le scroll
|
||||
overflow-y: auto !important; // Override simple-converted
|
||||
|
||||
.tab {
|
||||
padding: 0;
|
||||
height: auto !important; // Override simple-converted qui met height: auto
|
||||
|
||||
&:not(.active) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Assurer que les grilles peuvent scroller
|
||||
.grid, .grid-2col {
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.sheet-box {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Listes compactes dans les sections
|
||||
.compact-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li.item {
|
||||
padding: 0.2rem 0.4rem;
|
||||
margin-bottom: 0.15rem;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(139, 69, 19, 0.3);
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&.items-title-bg {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-color: rgba(139, 69, 19, 0.5);
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.3rem;
|
||||
|
||||
.item-name-label-header {
|
||||
color: #f5e6d3;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Contrôles d'items (edit, delete, equip, etc.)
|
||||
.item-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
justify-content: flex-end;
|
||||
|
||||
.item-control {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: #5a3a1a;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #2a1a0a;
|
||||
background: rgba(139, 69, 19, 0.2);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
&.item-edit {
|
||||
color: #4a7c59;
|
||||
|
||||
&:hover {
|
||||
color: #2d5a3a;
|
||||
background: rgba(74, 124, 89, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
&.item-delete {
|
||||
color: #a04040;
|
||||
|
||||
&:hover {
|
||||
color: #802020;
|
||||
background: rgba(160, 64, 64, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
&.item-equip {
|
||||
color: #6b5b3a;
|
||||
|
||||
&:hover {
|
||||
color: #4a3a1a;
|
||||
background: rgba(107, 91, 58, 0.2);
|
||||
}
|
||||
|
||||
i.fa-circle {
|
||||
color: #4a7c59;
|
||||
}
|
||||
|
||||
i.fa-genderless {
|
||||
color: #8a6a4a;
|
||||
}
|
||||
}
|
||||
|
||||
&.item-add {
|
||||
color: #4a7c59;
|
||||
|
||||
&:hover {
|
||||
color: #2d5a3a;
|
||||
background: rgba(74, 124, 89, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-controls-fixed {
|
||||
min-width: 5rem;
|
||||
max-width: 5rem;
|
||||
}
|
||||
|
||||
// Couleurs pour les labels et textes dans les onglets
|
||||
h4, h3, label, span.item-name-label, span.competence-name,
|
||||
.label-name, .generic-label, .item-field-label-short,
|
||||
.item-field-label-medium, .item-field-label-long,
|
||||
.short-label, .items-title-text {
|
||||
color: #3a2a1a !important;
|
||||
text-shadow: 0px 0px 1px rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
// Inputs dans le corps
|
||||
input[type="text"], input[type="number"], select {
|
||||
color: #2a1a0a;
|
||||
background: rgba(255, 250, 240, 0.8);
|
||||
border: 1px solid rgba(139, 69, 19, 0.5);
|
||||
height: 24px;
|
||||
padding: 0.15rem 0.3rem;
|
||||
line-height: 1.2;
|
||||
|
||||
&:focus {
|
||||
background: rgba(255, 255, 245, 0.95);
|
||||
border-color: rgba(139, 69, 19, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
// Titres de sections
|
||||
h3, h4 {
|
||||
font-weight: bold;
|
||||
color: #2a1a0a !important;
|
||||
}
|
||||
|
||||
// Titres de sections Santé, Âme, Combat
|
||||
h4.item-name-label.competence-name {
|
||||
font-size: 0.85rem;
|
||||
margin-top: 0.2rem;
|
||||
margin-bottom: 0.3rem;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
// Section grids pour Santé, Âme, Combat
|
||||
.section-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
padding: 0.3rem 0.4rem;
|
||||
margin-bottom: 0.3rem;
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
border: 2px solid rgba(139, 69, 19, 0.5);
|
||||
border-radius: 4px;
|
||||
|
||||
.section-title {
|
||||
font-size: 0.85rem;
|
||||
font-weight: bold;
|
||||
color: #f5e6d3;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin: 0 0 0.2rem 0;
|
||||
padding: 0.2rem 0.4rem;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-left: 3px solid rgba(139, 69, 19, 0.8);
|
||||
border-radius: 2px;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
.grid-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.25rem 0.4rem;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(139, 69, 19, 0.4);
|
||||
border-radius: 3px;
|
||||
|
||||
input {
|
||||
width: 60px;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
&.attr-row {
|
||||
grid-template-columns: 40px 1fr auto;
|
||||
gap: 0.5rem;
|
||||
|
||||
.item-name-img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid rgba(139, 69, 19, 0.5);
|
||||
}
|
||||
|
||||
.label-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
a {
|
||||
color: #f5e6d3;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.7);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
text-shadow: 0 0 4px rgba(255, 200, 100, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.label-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: #d4c5b0;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: #d4c5b0;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.value-display {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: 1px solid rgba(139, 69, 19, 0.6);
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
.malus-value {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
color: #ff9999;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: 1px solid rgba(200, 50, 50, 0.6);
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
&.damage-row {
|
||||
grid-template-columns: 120px 1fr;
|
||||
|
||||
.damage-label {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.damage-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
|
||||
input {
|
||||
width: 60px;
|
||||
min-width: 60px;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.plus-minus-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
border-radius: 4px;
|
||||
border: 2px solid rgba(139, 69, 19, 0.5);
|
||||
background: linear-gradient(to bottom, rgba(210, 180, 140, 0.9), rgba(180, 150, 110, 0.9));
|
||||
color: #2a1a0a;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(to bottom, rgba(230, 200, 160, 0.95), rgba(200, 170, 130, 0.95));
|
||||
border-color: rgba(139, 69, 19, 0.8);
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.soul-consumed-row {
|
||||
grid-template-columns: 120px 1fr;
|
||||
}
|
||||
|
||||
&.soul-malus-row {
|
||||
grid-template-columns: 90px 1fr;
|
||||
gap: 0.4rem;
|
||||
|
||||
.label-name {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.malus-value {
|
||||
font-size: 0.9rem;
|
||||
padding: 0.2rem 0.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.malus-row {
|
||||
grid-template-columns: 55px 60px 75px 50px;
|
||||
gap: 0.3rem;
|
||||
|
||||
.label-name {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
.malus-value {
|
||||
font-size: 0.9rem;
|
||||
padding: 0.2rem 0.3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.combat-grid {
|
||||
.grid-row.combat-stat {
|
||||
grid-template-columns: 85px 50px 60px 55px;
|
||||
gap: 0.4rem;
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.stat-base {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 0.85rem;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
padding: 0.2rem 0.3rem;
|
||||
border-radius: 3px;
|
||||
border: 1px solid rgba(139, 69, 19, 0.6);
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
input {
|
||||
width: 50px;
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-total {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 0.85rem;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
padding: 0.2rem 0.3rem;
|
||||
border-radius: 3px;
|
||||
border: 1px solid rgba(139, 69, 19, 0.6);
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.grid-row.protection-row {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
.protection-value {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
padding: 0.25rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: 1px solid rgba(139, 69, 19, 0.6);
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Boutons d'actions spéciales
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
padding: 0.3rem 0;
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.3rem 0;
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mounted-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.3rem 0.6rem;
|
||||
white-space: nowrap;
|
||||
|
||||
label {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== Creature Sheet Specific Styles ==================== */
|
||||
.fvtt-mournblade.actor.creature-sheet {
|
||||
|
||||
// Variant background pour creatures - teinte légèrement différente
|
||||
.background-sheet-header-creature {
|
||||
background: linear-gradient(135deg, rgba(0, 60, 0, 0.15) 0%, rgba(20, 80, 20, 0.1) 100%);
|
||||
border: 2px solid rgba(34, 139, 34, 0.5);
|
||||
border-radius: 4px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// Légère teinte verte pour les sections
|
||||
.sheet-box {
|
||||
&.color-bg-archetype {
|
||||
background: linear-gradient(135deg, rgba(0, 40, 0, 0.08) 0%, rgba(20, 60, 20, 0.05) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
// Header simplifié pour creatures
|
||||
.sheet-header {
|
||||
flex: 0 0 auto !important;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.profile-img {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid rgba(34, 139, 34, 0.6);
|
||||
object-fit: cover;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// Tabs avec teinte verte
|
||||
nav.tabs {
|
||||
.item.active {
|
||||
border-bottom-color: rgba(34, 139, 34, 0.8);
|
||||
color: #228b22;
|
||||
}
|
||||
}
|
||||
|
||||
// Section titles avec teinte verte
|
||||
.section-title {
|
||||
color: #1a5a1a;
|
||||
border-bottom-color: rgba(34, 139, 34, 0.3);
|
||||
}
|
||||
|
||||
// Items headers
|
||||
.items-title-bg {
|
||||
background: linear-gradient(135deg, rgba(0, 60, 0, 0.15) 0%, rgba(20, 80, 20, 0.1) 100%);
|
||||
border-bottom: 1px solid rgba(34, 139, 34, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// PNJ Sheet - Orange/Copper theme for visual distinction
|
||||
.fvtt-mournblade.actor.pnj-sheet {
|
||||
|
||||
// Variant background pour PNJs - teinte orange/cuivre
|
||||
.background-sheet-header-creature {
|
||||
background: linear-gradient(135deg, rgba(80, 40, 0, 0.15) 0%, rgba(100, 50, 0, 0.1) 100%);
|
||||
border: 2px solid rgba(205, 127, 50, 0.5);
|
||||
border-radius: 4px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// Légère teinte orange/cuivre pour les sections
|
||||
.sheet-box {
|
||||
&.color-bg-archetype {
|
||||
background: linear-gradient(135deg, rgba(60, 30, 0, 0.08) 0%, rgba(80, 40, 0, 0.05) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
// Header simplifié pour PNJs
|
||||
.sheet-header {
|
||||
flex: 0 0 auto !important;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.profile-img {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid rgba(205, 127, 50, 0.6);
|
||||
object-fit: cover;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// Tabs avec teinte orange/cuivre
|
||||
nav.tabs {
|
||||
.item.active {
|
||||
border-bottom-color: rgba(205, 127, 50, 0.8);
|
||||
color: #cd7f32;
|
||||
}
|
||||
}
|
||||
|
||||
// Section titles avec teinte orange/cuivre
|
||||
.section-title {
|
||||
color: #b8734d;
|
||||
border-bottom-color: rgba(205, 127, 50, 0.3);
|
||||
}
|
||||
|
||||
// Items headers
|
||||
.items-title-bg {
|
||||
background: linear-gradient(135deg, rgba(80, 40, 0, 0.15) 0%, rgba(100, 50, 0, 0.1) 100%);
|
||||
border-bottom: 1px solid rgba(205, 127, 50, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// GM Tools section - labels plus larges
|
||||
.gm-tools-section {
|
||||
.grid {
|
||||
.item-list .flexrow.item {
|
||||
.label-name {
|
||||
flex: 1;
|
||||
min-width: 12rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Alignment group in header
|
||||
.stat-group-alignment {
|
||||
.stat-values.alignment-values {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.alignment-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
|
||||
.alignment-label {
|
||||
font-weight: bold;
|
||||
min-width: 4rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.alignment-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-label-mini {
|
||||
font-size: 0.7rem;
|
||||
min-width: 2.5rem;
|
||||
}
|
||||
|
||||
.stat-input-mini {
|
||||
width: 2.5rem;
|
||||
height: 1.5rem;
|
||||
padding: 0.1rem 0.2rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
select.stat-input-mini {
|
||||
width: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.alignment-info {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 0.2rem;
|
||||
padding-top: 0.2rem;
|
||||
border-top: 1px solid rgba(139, 69, 19, 0.3);
|
||||
|
||||
.info-label {
|
||||
font-size: 0.75rem;
|
||||
color: #f5e6d3;
|
||||
|
||||
strong {
|
||||
color: #ffd700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== Sortilège Launch Button ==================== */
|
||||
.fvtt-mournblade.sheet {
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
padding: 4px 0;
|
||||
|
||||
.chat-card-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 12px;
|
||||
background: linear-gradient(135deg, #6b1a1a 0%, #8b2222 50%, #6b1a1a 100%);
|
||||
border: 1px solid #c0392b;
|
||||
border-radius: 4px;
|
||||
color: #f5e6d3;
|
||||
font-family: CentaurMT, serif;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
|
||||
i { color: #d4af37; }
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #8b2222 0%, #a52a2a 50%, #8b2222 100%);
|
||||
border-color: #d4af37;
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||
i { color: #ffd700; }
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: linear-gradient(135deg, #5a1010 0%, #6b1a1a 50%, #5a1010 100%);
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chat potion result — rune mini image and haut-parler styling
|
||||
.mournblade-chat-result {
|
||||
.detail-row {
|
||||
.rune-mini-img {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 2px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid rgba(139, 69, 19, 0.4);
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.potion-haut-parler {
|
||||
font-style: italic;
|
||||
color: #5a3a8a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post-item card: danger divider variant (for létal effects)
|
||||
.mournblade-item-card {
|
||||
.item-card-divider--danger {
|
||||
color: #8b0000;
|
||||
&::before, &::after {
|
||||
background: linear-gradient(90deg, transparent, #8b0000, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
/* ==================== Item Sheet Styles ==================== */
|
||||
|
||||
/* Item header with image and name */
|
||||
.fvtt-mournblade.item {
|
||||
/* Background pour toute la fiche d'item */
|
||||
background: url("../assets/ui/pc_sheet_bg.webp") repeat;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
/* AppV2 - Remove window content padding */
|
||||
.window-content {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* AppV2 - Main section structure */
|
||||
section {
|
||||
background: url("../assets/ui/pc_sheet_bg.webp") repeat-y;
|
||||
color: black;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* AppV2 Item Sheets - Disabled inputs readability */
|
||||
input:disabled,
|
||||
select:disabled {
|
||||
color: #000000;
|
||||
opacity: 0.8;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* Inputs and selects styling */
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
select {
|
||||
color: #000000;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
border: 1px solid #999999;
|
||||
margin: 0;
|
||||
padding: 2px 4px;
|
||||
font-family: "CentaurMT", serif;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
textarea {
|
||||
margin: 0;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin: 0 4px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex: 0 0 auto;
|
||||
border-bottom: 1px solid #999;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sheet-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.5rem;
|
||||
background: url("../assets/ui/pc_sheet_bg.webp") repeat;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.item-sheet-img {
|
||||
flex: 0 0 64px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: cover;
|
||||
border: 1px solid #999;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 8px rgba(255, 102, 0, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.header-fields {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
button {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #999;
|
||||
padding: 0.25rem 0.5rem;
|
||||
cursor: pointer;
|
||||
font-family: "CentaurMT", serif;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 102, 0, 0.2);
|
||||
box-shadow: 0 0 5px rgba(255, 102, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.chat-card-button {
|
||||
background: linear-gradient(to bottom, #21374afc 5%, #152833ab 100%);
|
||||
border: 2px ridge #846109;
|
||||
color: #d4b5a8;
|
||||
padding: 0.3rem 0.5rem;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
|
||||
i {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(to bottom, #800000 5%, #3e0101 100%);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 0 8px rgba(128, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
&:active {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.sheet-header h1.charname {
|
||||
height: 50px;
|
||||
padding: 0;
|
||||
margin: 5px 0;
|
||||
border-bottom: 0;
|
||||
font-weight: bold;
|
||||
font-size: 2rem;
|
||||
font-family: "CentaurMT";
|
||||
}
|
||||
|
||||
.sheet-header h1.charname input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
font-family: "CentaurMT";
|
||||
font-size: 2rem;
|
||||
text-align: left;
|
||||
border: 0 none;
|
||||
|
||||
&:focus {
|
||||
outline: 1px solid #ff6600;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tabs - Modern style matching actor sheets */
|
||||
nav.tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #7a7971;
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
background-color: #1c1c1c;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
nav.tabs a.item {
|
||||
padding: 6px 12px;
|
||||
color: #d4af37;
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px 4px 0 0;
|
||||
margin-right: 4px;
|
||||
transition: all 0.2s;
|
||||
|
||||
i {
|
||||
display: none; // Hide icons to match actor sheets
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(212, 175, 55, 0.1);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #2a2a2a;
|
||||
border-bottom-color: #d4af37;
|
||||
color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tab content */
|
||||
.tab {
|
||||
display: none;
|
||||
padding: 4px 8px;
|
||||
overflow-y: auto;
|
||||
flex: 1 1 auto;
|
||||
|
||||
&.active {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* Item list in details tab */
|
||||
.item-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 2px;
|
||||
padding: 2px 4px;
|
||||
min-height: 24px;
|
||||
border-bottom: none;
|
||||
|
||||
&.flexrow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin-top: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #2a1a0a;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Labels */
|
||||
.generic-label {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
font-weight: 700;
|
||||
color: #464331c4;
|
||||
font-size: 0.9rem;
|
||||
font-family: "CentaurMT", serif;
|
||||
margin: 0;
|
||||
padding: 0 4px 0 0;
|
||||
}
|
||||
|
||||
/* Field labels */
|
||||
.item-field-label-short {
|
||||
flex: 0 0 60px;
|
||||
max-width: 60px;
|
||||
}
|
||||
|
||||
.item-field-label-medium {
|
||||
flex: 0 0 100px;
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.item-field-label-long {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.item-field-label-long1 {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.item-field-label-long2 {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
max-width: 250px;
|
||||
}
|
||||
.item-field-label-long3 {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
white-space: normal;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.numeric-input {
|
||||
text-align: center;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
/* Editor fields */
|
||||
.editor {
|
||||
height: 300px;
|
||||
border: 1px solid #999;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
|
||||
.editor-content {
|
||||
height: 100%;
|
||||
padding: 0.5rem;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================= */
|
||||
/* Potion Item Sheet */
|
||||
/* ============================================= */
|
||||
.potion-content {
|
||||
.potion-header-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
background: rgba(139, 0, 0, 0.08);
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(139, 0, 0, 0.2);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.potion-statut-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: bold;
|
||||
|
||||
&.statut-efficace { background: rgba(0,128,0,0.15); color: #155215; border: 1px solid #4a904a; }
|
||||
&.statut-heroique { background: rgba(180,120,0,0.15); color: #7a5000; border: 1px solid #c89000; }
|
||||
&.statut-inefficace { background: rgba(100,100,100,0.15); color: #555; border: 1px solid #888; }
|
||||
&.statut-poison { background: rgba(100,0,100,0.15); color: #4a004a; border: 1px solid #880088; }
|
||||
&.statut-inconnue { background: rgba(50,50,50,0.1); color: #666; border: 1px solid #aaa; }
|
||||
}
|
||||
}
|
||||
|
||||
/* Potion in chat */
|
||||
.potion-rune-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.4rem;
|
||||
background: rgba(0,0,0,0.04);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 0.35rem;
|
||||
|
||||
.potion-rune-name { font-weight: bold; }
|
||||
.potion-rune-seuil { font-size: 0.85rem; color: #666; }
|
||||
.potion-haut-parler { font-size: 0.85rem; color: #555; font-style: italic; }
|
||||
}
|
||||
|
||||
.potion-result-gm {
|
||||
border: 1px dashed rgba(139,0,0,0.4);
|
||||
background: rgba(139,0,0,0.05);
|
||||
border-radius: 4px;
|
||||
padding: 0.4rem;
|
||||
|
||||
.potion-result-title {
|
||||
font-size: 0.95rem;
|
||||
margin: 0 0 0.3rem;
|
||||
}
|
||||
.potion-heroique { color: #8B6900; }
|
||||
.potion-efficace { color: #155215; }
|
||||
.potion-inefficace { color: #555; }
|
||||
.potion-poison { color: #660066; }
|
||||
}
|
||||
|
||||
.potion-result-player {
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Potion dialog */
|
||||
.potion-dialog {
|
||||
.potion-dialog-section {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.potion-dialog-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.15);
|
||||
padding-bottom: 0.2rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.potion-runes-table {
|
||||
width: 100%;
|
||||
font-size: 0.85rem;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
th, td {
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.potion-rune-row:hover { background: rgba(0,0,0,0.04); }
|
||||
.potion-rune-radio { cursor: pointer; }
|
||||
|
||||
.rune-mini-img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid rgba(139, 69, 19, 0.5);
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.potion-summary {
|
||||
background: rgba(0,0,0,0.05);
|
||||
border-radius: 4px;
|
||||
padding: 0.4rem 0.6rem;
|
||||
font-size: 0.85rem;
|
||||
|
||||
.summary-row { margin-bottom: 0.2rem; }
|
||||
.summary-label { font-weight: bold; min-width: 150px; }
|
||||
}
|
||||
|
||||
.potion-info-note {
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
gap: 0.4rem;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Enchantement de la Loi */
|
||||
.enchantement-section {
|
||||
margin-top: 0.6rem;
|
||||
padding: 0.5rem 0.6rem;
|
||||
background: rgba(255, 215, 0, 0.05);
|
||||
border: 1px solid rgba(255, 215, 0, 0.3);
|
||||
border-radius: 4px;
|
||||
|
||||
.section-title-small {
|
||||
font-size: 0.9rem;
|
||||
margin: 0 0 0.4rem 0;
|
||||
color: #a07800;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
border-bottom: 1px solid rgba(255, 215, 0, 0.2);
|
||||
padding-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.enchant-badge {
|
||||
display: inline-block;
|
||||
background: gold;
|
||||
color: #333;
|
||||
font-size: 0.7rem;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 8px;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.enchant-none {
|
||||
color: #888;
|
||||
font-size: 0.85rem;
|
||||
margin: 0;
|
||||
padding: 0.2rem 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Main LESS file for Mournblade system
|
||||
// Importing base styles and component-specific styles
|
||||
|
||||
@import "simple-converted";
|
||||
@import "item-styles";
|
||||
@import "actor-styles";
|
||||
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* Extend the basic ActorSheet with some very simple modifications
|
||||
* @extends {ActorSheet}
|
||||
* @extends {ActorSheetV2}
|
||||
*/
|
||||
|
||||
import { MournbladeUtility } from "./mournblade-utility.js";
|
||||
import { MournbladeRollDialog } from "./mournblade-roll-dialog.js";
|
||||
|
||||
/* -------------------------------------------- */
|
||||
export class MournbladeActorSheet extends foundry.appv1.sheets.ActorSheet {
|
||||
export class MournbladeActorSheet extends foundry.applications.sheets.ActorSheetV2 {
|
||||
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
@@ -0,0 +1,157 @@
|
||||
import { MournbladeUtility } from "../mournblade-utility.js"
|
||||
|
||||
export default class MournbladeEnchantementDialog {
|
||||
|
||||
static _normalize(str) {
|
||||
return (str ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/œ/g, "oe")
|
||||
.replace(/æ/g, "ae")
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, " ")
|
||||
.trim()
|
||||
}
|
||||
|
||||
static _findCompetence(actor, ...keywords) {
|
||||
const normKeys = keywords.map(k => MournbladeEnchantementDialog._normalize(k))
|
||||
return actor.items.find(item => {
|
||||
if (item.type !== "competence") return false
|
||||
const norm = MournbladeEnchantementDialog._normalize(item.name)
|
||||
return normKeys.every(k => norm.includes(k))
|
||||
}) ?? null
|
||||
}
|
||||
|
||||
static async create(actor, item) {
|
||||
const normalize = MournbladeEnchantementDialog._normalize.bind(MournbladeEnchantementDialog)
|
||||
const findComp = (...kw) => MournbladeEnchantementDialog._findCompetence(actor, ...kw)
|
||||
|
||||
const ameDisponible = Math.max(0, (actor.system.ame.currentmax - actor.system.ame.value))
|
||||
const aspect = actor.system.balance.aspect ?? 0
|
||||
|
||||
// Skill lookups
|
||||
const savoirRunesComp = findComp("rune")
|
||||
const hautParlerComp = findComp("haut", "parler")
|
||||
const artisanatComp = findComp("savoir", "artisanat")
|
||||
const claAttr = actor.system.attributs?.clairvoyance
|
||||
|
||||
// Prerequisite: Rune de la Loi in inventory
|
||||
const hasRuneLoi = actor.items.some(i => {
|
||||
if (i.type !== "rune") return false
|
||||
return normalize(i.name).includes("loi")
|
||||
})
|
||||
|
||||
const savoirRunesNiveau = savoirRunesComp ? (savoirRunesComp.system.niveau ?? 0) : null
|
||||
const hautParlerNiveau = hautParlerComp ? (hautParlerComp.system.niveau ?? 0) : null
|
||||
const artisanatNiveau = artisanatComp ? (artisanatComp.system.niveau ?? 0) : null
|
||||
const claValeur = claAttr ? (claAttr.value ?? 0) : 0
|
||||
|
||||
// Limit: CLA + Savoir:Runes is capped by min(Haut-Parler, Artisanat) if those skills exist
|
||||
const limiteur = (hautParlerNiveau !== null && artisanatNiveau !== null)
|
||||
? Math.min(hautParlerNiveau, artisanatNiveau)
|
||||
: (hautParlerNiveau ?? artisanatNiveau ?? null)
|
||||
|
||||
const context = {
|
||||
actorImg: actor.img,
|
||||
actorName: actor.name,
|
||||
itemImg: item.img,
|
||||
itemName: item.name,
|
||||
itemType: item.type,
|
||||
itemId: item.id,
|
||||
ameDisponible,
|
||||
aspect,
|
||||
hasRuneLoi,
|
||||
savoirRunesNiveau,
|
||||
hautParlerNiveau,
|
||||
artisanatNiveau,
|
||||
claValeur,
|
||||
limiteur,
|
||||
modOptions: Array.from({ length: 21 }, (_, i) => i - 10),
|
||||
enchantementActif: item.system.enchantementLoi?.actif ?? false,
|
||||
enchantementBonus: item.system.enchantementLoi?.bonus ?? 0,
|
||||
enchantementAntiChaos: item.system.enchantementLoi?.antiChaos ?? false,
|
||||
}
|
||||
|
||||
const prerequisOk = hasRuneLoi
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-mournblade/templates/dialog-enchantement.hbs",
|
||||
context
|
||||
)
|
||||
|
||||
Hooks.once("renderDialogV2", (_app, html) => {
|
||||
const form = html.querySelector ? html : html[0]
|
||||
MournbladeEnchantementDialog._attachListeners(form, ameDisponible, claValeur, savoirRunesNiveau, limiteur, prerequisOk)
|
||||
})
|
||||
|
||||
return foundry.applications.api.DialogV2.wait({
|
||||
window: { title: `Enchanter : ${item.name}`, icon: "fa-solid fa-star" },
|
||||
classes: ["mournblade-roll-dialog"],
|
||||
position: { width: 520 },
|
||||
modal: false,
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
action: "enchanter",
|
||||
label: "Enchanter",
|
||||
icon: "fa-solid fa-star",
|
||||
default: true,
|
||||
callback: async (event, button, dialog) => {
|
||||
const elems = button.form.elements
|
||||
const ptsAme = parseInt(elems["ptsAme"]?.value ?? 5) || 5
|
||||
const antiChaos = elems["antiChaos"]?.value === "true"
|
||||
const modificateur = parseInt(elems["modificateur"]?.value ?? 0) || 0
|
||||
await MournbladeUtility.rollEnchantement({
|
||||
actor,
|
||||
item,
|
||||
ptsAme,
|
||||
antiChaos,
|
||||
modificateur,
|
||||
savoirRunesComp,
|
||||
hautParlerComp,
|
||||
artisanatComp,
|
||||
claValeur,
|
||||
limiteur,
|
||||
})
|
||||
}
|
||||
},
|
||||
],
|
||||
rejectClose: false,
|
||||
})
|
||||
}
|
||||
|
||||
static _attachListeners(html, ameDisponible, claValeur, savoirRunesNiveau, limiteur, prerequisOk = true) {
|
||||
const enchanterBtn = html.querySelector('button[data-action="enchanter"]')
|
||||
if (enchanterBtn) enchanterBtn.disabled = !prerequisOk
|
||||
|
||||
const diffEl = html.querySelector('#enchant-difficulte')
|
||||
const bonusEl = html.querySelector('#enchant-bonus-preview')
|
||||
const warnAmeEl = html.querySelector('#enchant-ame-warn')
|
||||
const warnLimitEl = html.querySelector('#enchant-limit-warn')
|
||||
const totalEl = html.querySelector('#enchant-total-dice')
|
||||
|
||||
const recalculate = () => {
|
||||
const ptsAme = parseInt(html.querySelector('[name="ptsAme"]')?.value ?? 5) || 0
|
||||
const difficulte = ptsAme
|
||||
const bonus = Math.floor(ptsAme / 5)
|
||||
const savoir = savoirRunesNiveau ?? 0
|
||||
const basePool = claValeur + savoir
|
||||
const effectivePool = limiteur !== null ? Math.min(basePool, limiteur) : basePool
|
||||
|
||||
if (diffEl) diffEl.textContent = difficulte
|
||||
if (bonusEl) bonusEl.textContent = `+${bonus}`
|
||||
if (totalEl) totalEl.textContent = effectivePool
|
||||
|
||||
if (warnAmeEl) warnAmeEl.style.display = ptsAme > ameDisponible ? "" : "none"
|
||||
if (warnLimitEl && limiteur !== null)
|
||||
warnLimitEl.style.display = basePool > limiteur ? "" : "none"
|
||||
}
|
||||
|
||||
const ptsAmeEl = html.querySelector('[name="ptsAme"]')
|
||||
if (ptsAmeEl) {
|
||||
ptsAmeEl.addEventListener('input', recalculate)
|
||||
ptsAmeEl.addEventListener('change', recalculate)
|
||||
}
|
||||
recalculate()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { MournbladeUtility } from "../mournblade-utility.js"
|
||||
|
||||
export default class MournbladeInvocationDemonDialog {
|
||||
|
||||
/**
|
||||
* Normalize a string for fuzzy matching: lowercase, remove diacritics,
|
||||
* replace œ/æ ligatures, collapse whitespace and punctuation.
|
||||
*/
|
||||
static _normalize(str) {
|
||||
return (str ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/œ/g, "oe")
|
||||
.replace(/æ/g, "ae")
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "") // strip combining diacritics
|
||||
.replace(/[^a-z0-9]+/g, " ") // replace any non-alnum with space
|
||||
.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first competence item whose name contains all of the given keywords.
|
||||
* Keywords are normalized before comparison.
|
||||
*/
|
||||
static _findCompetence(actor, ...keywords) {
|
||||
const normKeys = keywords.map(k => MournbladeInvocationDemonDialog._normalize(k))
|
||||
return actor.items.find(item => {
|
||||
if (item.type !== "competence") return false
|
||||
const norm = MournbladeInvocationDemonDialog._normalize(item.name)
|
||||
return normKeys.every(k => norm.includes(k))
|
||||
}) ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the actor has a capacite or don whose name contains all given keywords.
|
||||
*/
|
||||
static _hasCapacite(actor, ...keywords) {
|
||||
const normKeys = keywords.map(k => MournbladeInvocationDemonDialog._normalize(k))
|
||||
return actor.items.some(item => {
|
||||
if (item.type !== "capacite" && item.type !== "don") return false
|
||||
const norm = MournbladeInvocationDemonDialog._normalize(item.name)
|
||||
return normKeys.every(k => norm.includes(k))
|
||||
})
|
||||
}
|
||||
|
||||
static async create(actor, rollData) {
|
||||
const ameDisponible = Math.max(0, (actor.system.ame.currentmax - actor.system.ame.value))
|
||||
|
||||
// Robust skill detection: partial keyword matching, diacritic-insensitive
|
||||
const coercitionComp = MournbladeInvocationDemonDialog._findCompetence(actor, "coercition")
|
||||
const hautParlerComp = MournbladeInvocationDemonDialog._findCompetence(actor, "haut", "parler")
|
||||
const loiChaosComp = MournbladeInvocationDemonDialog._findCompetence(actor, "loi", "chaos")
|
||||
|
||||
// Check prerequisites — robust capacite/rune detection
|
||||
const isChaotique = actor.system.balance.chaos > actor.system.balance.loi
|
||||
const hasOeilSorcier = MournbladeInvocationDemonDialog._hasCapacite(actor, "oeil", "sorcier")
|
||||
const hasRuneChaos = actor.items.some(i => {
|
||||
if (i.type !== "rune") return false
|
||||
return MournbladeInvocationDemonDialog._normalize(i.name).includes("chaos")
|
||||
})
|
||||
const prerequisOk = isChaotique && hasOeilSorcier && hasRuneChaos
|
||||
|
||||
// Auto-detect chaos link bonuses
|
||||
const aspectGe8 = (actor.system.balance.aspect ?? 0) >= 8
|
||||
// hasPacte: true if actor has a Pacte item whose associated deity matches a demon
|
||||
// (simplified: just expose it as a selectable option — can't auto-detect)
|
||||
const hasPacte = false
|
||||
|
||||
const context = {
|
||||
...rollData,
|
||||
img: actor.img,
|
||||
name: actor.name,
|
||||
ameDisponible,
|
||||
modOptions: Array.from({ length: 21 }, (_, i) => i - 10),
|
||||
coercitionNiveau: coercitionComp ? coercitionComp.system.niveau : null,
|
||||
hautParlerNiveau: hautParlerComp ? hautParlerComp.system.niveau : null,
|
||||
loiChaosNiveau: loiChaosComp ? loiChaosComp.system.niveau : null,
|
||||
isChaotique,
|
||||
hasOeilSorcier,
|
||||
hasRuneChaos,
|
||||
prerequisOk,
|
||||
aspectGe8,
|
||||
hasPacte,
|
||||
}
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-mournblade/templates/dialog-invocation-demon.hbs",
|
||||
context
|
||||
)
|
||||
|
||||
Hooks.once("renderDialogV2", (_app, html) => {
|
||||
const form = html.querySelector ? html : html[0]
|
||||
MournbladeInvocationDemonDialog._attachListeners(form, ameDisponible, prerequisOk)
|
||||
})
|
||||
|
||||
return foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Invoquer un Démon", icon: "fa-solid fa-skull" },
|
||||
classes: ["mournblade-roll-dialog"],
|
||||
position: { width: 560 },
|
||||
modal: false,
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
action: "invoquer",
|
||||
label: "Invoquer",
|
||||
icon: "fa-solid fa-skull",
|
||||
default: true,
|
||||
callback: async (event, button, dialog) => {
|
||||
MournbladeInvocationDemonDialog._updateRollData(rollData, button.form.elements, actor, {
|
||||
coercitionComp, hautParlerComp, loiChaosComp
|
||||
})
|
||||
await MournbladeUtility.rollInvocationDemon(rollData)
|
||||
}
|
||||
},
|
||||
],
|
||||
rejectClose: false,
|
||||
})
|
||||
}
|
||||
|
||||
static _calculateSeuil(html) {
|
||||
const get = (name) => parseInt(html.querySelector(`[name="${name}"]`)?.value ?? 0) || 0
|
||||
const seuil = get("seuil_nature")
|
||||
+ get("seuil_traits")
|
||||
+ get("seuil_augmentation")
|
||||
+ get("seuil_service")
|
||||
+ get("seuil_duree")
|
||||
+ get("seuil_marche")
|
||||
+ get("seuil_chaos")
|
||||
+ get("seuil_sacrifice")
|
||||
return Math.max(1, seuil)
|
||||
}
|
||||
|
||||
static _attachListeners(html, ameDisponible, prerequisOk = true) {
|
||||
const invoquerBtn = html.querySelector('button[data-action="invoquer"]')
|
||||
if (invoquerBtn) invoquerBtn.disabled = !prerequisOk
|
||||
|
||||
const coutEl = html.querySelector('#invoc-demon-cout')
|
||||
const totalEl = html.querySelector('#invoc-demon-seuil-total')
|
||||
const hiddenEl = html.querySelector('#invoc-demon-seuil-hidden')
|
||||
const warnEl = html.querySelector('#invoc-demon-ame-warn')
|
||||
|
||||
const criteriaNames = [
|
||||
"seuil_nature", "seuil_traits", "seuil_augmentation",
|
||||
"seuil_service", "seuil_duree", "seuil_marche",
|
||||
"seuil_chaos", "seuil_sacrifice"
|
||||
]
|
||||
|
||||
const recalculate = () => {
|
||||
const seuil = MournbladeInvocationDemonDialog._calculateSeuil(html)
|
||||
if (totalEl) totalEl.textContent = seuil
|
||||
if (hiddenEl) hiddenEl.value = seuil
|
||||
if (coutEl) coutEl.textContent = seuil
|
||||
if (warnEl) warnEl.style.display = seuil > ameDisponible ? "" : "none"
|
||||
}
|
||||
|
||||
for (const name of criteriaNames) {
|
||||
const el = html.querySelector(`[name="${name}"]`)
|
||||
if (el) el.addEventListener('change', recalculate)
|
||||
if (el && el.type === 'number') el.addEventListener('input', recalculate)
|
||||
}
|
||||
|
||||
recalculate()
|
||||
}
|
||||
|
||||
static _updateRollData(rollData, formElements, actor, { coercitionComp }) {
|
||||
const seuil = parseInt(formElements['seuil']?.value ?? 5)
|
||||
const modificateur = parseInt(formElements['modificateur']?.value ?? 0)
|
||||
|
||||
rollData.invocationSeuil = seuil
|
||||
rollData.invocationSoulCost = seuil
|
||||
rollData.difficulte = seuil
|
||||
rollData.modificateur = modificateur
|
||||
rollData.competence = coercitionComp ?? null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { MournbladeUtility } from "../mournblade-utility.js"
|
||||
|
||||
export default class MournbladeInvocationDialog {
|
||||
|
||||
/**
|
||||
* Normalize a string for fuzzy matching: lowercase, remove diacritics,
|
||||
* replace œ/æ ligatures, collapse whitespace and punctuation.
|
||||
*/
|
||||
static _normalize(str) {
|
||||
return (str ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/œ/g, "oe")
|
||||
.replace(/æ/g, "ae")
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, " ")
|
||||
.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first competence item whose name contains all of the given keywords.
|
||||
*/
|
||||
static _findCompetence(actor, ...keywords) {
|
||||
const normKeys = keywords.map(k => MournbladeInvocationDialog._normalize(k))
|
||||
return actor.items.find(item => {
|
||||
if (item.type !== "competence") return false
|
||||
const norm = MournbladeInvocationDialog._normalize(item.name)
|
||||
return normKeys.every(k => norm.includes(k))
|
||||
}) ?? null
|
||||
}
|
||||
|
||||
static async create(actor, rollData) {
|
||||
const ameDisponible = Math.max(0, (actor.system.ame.currentmax - actor.system.ame.value))
|
||||
const maxExtra = Math.max(0, ameDisponible - 15)
|
||||
|
||||
// Detect elemental pacte bonus — the bonus is always available,
|
||||
// but we display it dynamically based on chosen element.
|
||||
// We pass the pactes to let JS listeners detect the match.
|
||||
const pactes = actor.getPactes().map(p => ({
|
||||
name: p.name,
|
||||
allegeance: (p.system.allegeance || "").toLowerCase()
|
||||
}))
|
||||
|
||||
// Robust skill detection: partial keyword matching, diacritic-insensitive
|
||||
const hautParlerComp = MournbladeInvocationDialog._findCompetence(actor, "haut", "parler")
|
||||
const seigneursElemComp = MournbladeInvocationDialog._findCompetence(actor, "seigneurs", "elementaires")
|
||||
|
||||
const context = {
|
||||
...rollData,
|
||||
img: actor.img,
|
||||
name: actor.name,
|
||||
ameDisponible,
|
||||
ameExtraOptions: Array.from({ length: maxExtra + 1 }, (_, i) => i),
|
||||
modOptions: Array.from({ length: 21 }, (_, i) => i - 10),
|
||||
hautParlerNiveau: hautParlerComp ? hautParlerComp.system.niveau : null,
|
||||
seigneursElemNiveau: seigneursElemComp ? seigneursElemComp.system.niveau : null,
|
||||
bonusPacte: false,
|
||||
pactes,
|
||||
}
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-mournblade/templates/dialog-invocation-elementaire.hbs",
|
||||
context
|
||||
)
|
||||
|
||||
Hooks.once("renderDialogV2", (_app, html) => {
|
||||
const form = html.querySelector ? html : html[0]
|
||||
MournbladeInvocationDialog._attachListeners(form, pactes)
|
||||
})
|
||||
|
||||
return foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Invoquer un Élémentaire", icon: "fa-solid fa-wind" },
|
||||
classes: ["mournblade-roll-dialog"],
|
||||
position: { width: 480 },
|
||||
modal: false,
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
action: "invoquer",
|
||||
label: "Invoquer",
|
||||
icon: "fa-solid fa-wind",
|
||||
default: true,
|
||||
callback: async (event, button, dialog) => {
|
||||
MournbladeInvocationDialog._updateRollData(rollData, button.form.elements, actor, pactes)
|
||||
await MournbladeUtility.rollInvocationElementaire(rollData)
|
||||
}
|
||||
},
|
||||
],
|
||||
rejectClose: false,
|
||||
})
|
||||
}
|
||||
|
||||
static _elementKeywords = {
|
||||
air: ["air", "vent", "sylphe", "seigneur de l'air"],
|
||||
terre: ["terre", "gnome", "seigneur de la terre"],
|
||||
feu: ["feu", "flamme", "salamandre", "seigneur du feu"],
|
||||
eau: ["eau", "ondine", "seigneur de l'eau"],
|
||||
}
|
||||
|
||||
static _hasPacteBonus(element, pactes) {
|
||||
const keywords = MournbladeInvocationDialog._elementKeywords[element] || []
|
||||
return pactes.some(p => keywords.some(kw => p.allegeance.includes(kw)))
|
||||
}
|
||||
|
||||
static _attachListeners(html, pactes) {
|
||||
const tierSeuils = { mineur: 15, median: 20, majeur: 25 }
|
||||
const tierTemps = { mineur: "1 tour", median: "1 minute", majeur: "1 heure" }
|
||||
|
||||
const recalculate = () => {
|
||||
const element = html.querySelector('[name="element"]')?.value ?? "air"
|
||||
const tier = html.querySelector('[name="tier"]')?.value ?? "mineur"
|
||||
const ameExtra = parseInt(html.querySelector('[name="ameExtra"]')?.value ?? 0)
|
||||
|
||||
const seuilBase = tierSeuils[tier] ?? 15
|
||||
const seuil = seuilBase + ameExtra
|
||||
const hasPacte = MournbladeInvocationDialog._hasPacteBonus(element, pactes)
|
||||
|
||||
const seuilEl = html.querySelector('#invoc-seuil')
|
||||
const coutEl = html.querySelector('#invoc-cout')
|
||||
const tempsEl = html.querySelector('#invoc-temps')
|
||||
const pacteBanner = html.querySelector('.invoc-bonus-pacte')
|
||||
|
||||
if (seuilEl) seuilEl.textContent = seuil + (hasPacte ? " (-5 avec bonus Pacte)" : "")
|
||||
if (coutEl) coutEl.textContent = seuil
|
||||
if (tempsEl) tempsEl.textContent = tierTemps[tier] ?? "1 tour"
|
||||
if (pacteBanner) pacteBanner.style.display = hasPacte ? "" : "none"
|
||||
}
|
||||
|
||||
html.querySelectorAll('[name="element"], [name="tier"], [name="ameExtra"]').forEach(el => {
|
||||
el.addEventListener('change', recalculate)
|
||||
})
|
||||
recalculate()
|
||||
}
|
||||
|
||||
static _updateRollData(rollData, formElements, actor, pactes) {
|
||||
const element = formElements['element']?.value ?? "air"
|
||||
const tier = formElements['tier']?.value ?? "mineur"
|
||||
const ameExtra = parseInt(formElements['ameExtra']?.value ?? 0)
|
||||
const modificateur = parseInt(formElements['modificateur']?.value ?? 0)
|
||||
|
||||
const tierSeuils = { mineur: 15, median: 20, majeur: 25 }
|
||||
const seuilBase = tierSeuils[tier] ?? 15
|
||||
const seuil = seuilBase + ameExtra
|
||||
|
||||
rollData.invocationElement = element
|
||||
rollData.invocationTier = tier
|
||||
rollData.invocationSeuil = seuil
|
||||
rollData.invocationAmeExtra = ameExtra
|
||||
rollData.invocationSoulCost = seuil
|
||||
rollData.difficulte = seuil
|
||||
rollData.modificateur = modificateur
|
||||
rollData.bonusPacte = MournbladeInvocationDialog._hasPacteBonus(element, pactes) ? 5 : 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { MournbladeUtility } from "../mournblade-utility.js"
|
||||
|
||||
export default class MournbladeInvocationEspritDialog {
|
||||
|
||||
static _normalize(str) {
|
||||
return (str ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/œ/g, "oe").replace(/æ/g, "ae")
|
||||
.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, " ").trim()
|
||||
}
|
||||
|
||||
static _findCompetence(actor, ...keywords) {
|
||||
const normKeys = keywords.map(k => MournbladeInvocationEspritDialog._normalize(k))
|
||||
return actor.items.find(item => {
|
||||
if (item.type !== "competence") return false
|
||||
const norm = MournbladeInvocationEspritDialog._normalize(item.name)
|
||||
return normKeys.every(k => norm.includes(k))
|
||||
}) ?? null
|
||||
}
|
||||
|
||||
/** Seuil and soul cost per puissance */
|
||||
static SEUILS = { mineur: 15, median: 20, majeur: 25 }
|
||||
|
||||
static async create(actor, rollData) {
|
||||
const ameDisponible = Math.max(0, actor.system.ame.currentmax - actor.system.ame.value)
|
||||
|
||||
const persuasionComp = MournbladeInvocationEspritDialog._findCompetence(actor, "persuasion")
|
||||
const hautParlerComp = MournbladeInvocationEspritDialog._findCompetence(actor, "haut", "parler")
|
||||
const loiChaosComp = MournbladeInvocationEspritDialog._findCompetence(actor, "loi", "chaos")
|
||||
|
||||
const isLoyal = actor.system.balance.loi > actor.system.balance.chaos
|
||||
const hasRuneLoi = actor.items.some(i =>
|
||||
i.type === "rune" && MournbladeInvocationEspritDialog._normalize(i.name).includes("loi")
|
||||
)
|
||||
const prerequisOk = isLoyal && hasRuneLoi
|
||||
|
||||
const automatonTypes = {
|
||||
combat: "Combat",
|
||||
voyage: "Voyage",
|
||||
perception: "Perception",
|
||||
restauration:"Restauration",
|
||||
reparateur: "Réparateur",
|
||||
}
|
||||
|
||||
const context = {
|
||||
...rollData,
|
||||
img: actor.img,
|
||||
name: actor.name,
|
||||
ameDisponible,
|
||||
treValeur: actor.system.attributs?.tre?.value ?? 0,
|
||||
persuasionNiveau: persuasionComp?.system?.niveau ?? null,
|
||||
hautParlerNiveau: hautParlerComp?.system?.niveau ?? null,
|
||||
loiChaosNiveau: loiChaosComp?.system?.niveau ?? null,
|
||||
isLoyal,
|
||||
hasRuneLoi,
|
||||
prerequisOk,
|
||||
automatonTypes,
|
||||
seuils: MournbladeInvocationEspritDialog.SEUILS,
|
||||
modOptions: Array.from({ length: 21 }, (_, i) => i - 10),
|
||||
}
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-mournblade/templates/dialog-invocation-esprit.hbs",
|
||||
context
|
||||
)
|
||||
|
||||
Hooks.once("renderDialogV2", (_app, html) => {
|
||||
const form = html.querySelector ? html : html[0]
|
||||
MournbladeInvocationEspritDialog._attachListeners(form, ameDisponible, prerequisOk)
|
||||
})
|
||||
|
||||
return foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Invoquer un Esprit de la Loi", icon: "fa-solid fa-star" },
|
||||
classes: ["mournblade-roll-dialog"],
|
||||
position: { width: 520 },
|
||||
modal: false,
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
action: "invoquer",
|
||||
label: "Invoquer",
|
||||
icon: "fa-solid fa-star",
|
||||
default: true,
|
||||
callback: async (event, button, dialog) => {
|
||||
MournbladeInvocationEspritDialog._updateRollData(rollData, button.form.elements, actor, {
|
||||
persuasionComp, hautParlerComp, loiChaosComp
|
||||
})
|
||||
await MournbladeUtility.rollInvocationEsprit(rollData)
|
||||
}
|
||||
},
|
||||
],
|
||||
rejectClose: false,
|
||||
})
|
||||
}
|
||||
|
||||
static _attachListeners(html, ameDisponible, prerequisOk = true) {
|
||||
const invoquerBtn = html.querySelector('button[data-action="invoquer"]')
|
||||
if (invoquerBtn) invoquerBtn.disabled = !prerequisOk
|
||||
|
||||
const puissanceEl = html.querySelector('[name="puissance"]')
|
||||
const seuilEl = html.querySelector('#esprit-seuil-total')
|
||||
const coutEl = html.querySelector('#esprit-cout-ame')
|
||||
const hiddenEl = html.querySelector('#esprit-seuil-hidden')
|
||||
const warnEl = html.querySelector('#esprit-ame-warn')
|
||||
const dureeEl = html.querySelector('#esprit-duree')
|
||||
|
||||
const DUREE = { mineur: "1 heure", median: "1 jour", majeur: "1 semaine" }
|
||||
|
||||
const recalculate = () => {
|
||||
const puissance = puissanceEl?.value ?? "mineur"
|
||||
const seuil = MournbladeInvocationEspritDialog.SEUILS[puissance] ?? 15
|
||||
if (seuilEl) seuilEl.textContent = seuil
|
||||
if (coutEl) coutEl.textContent = seuil
|
||||
if (hiddenEl) hiddenEl.value = seuil
|
||||
if (dureeEl) dureeEl.textContent = DUREE[puissance] ?? "1 heure"
|
||||
if (warnEl) warnEl.style.display = seuil > ameDisponible ? "" : "none"
|
||||
}
|
||||
|
||||
if (puissanceEl) puissanceEl.addEventListener("change", recalculate)
|
||||
recalculate()
|
||||
}
|
||||
|
||||
static _updateRollData(rollData, formElements, actor, { persuasionComp }) {
|
||||
const seuil = parseInt(formElements["seuil"]?.value ?? 15)
|
||||
const modificateur = parseInt(formElements["modificateur"]?.value ?? 0)
|
||||
const automatonType = formElements["automatonType"]?.value ?? "combat"
|
||||
const puissance = formElements["puissance"]?.value ?? "mineur"
|
||||
|
||||
rollData.invocationSeuil = seuil
|
||||
rollData.invocationSoulCost = seuil
|
||||
rollData.difficulte = seuil
|
||||
rollData.modificateur = modificateur
|
||||
rollData.competence = persuasionComp ?? null
|
||||
rollData.automatonType = automatonType
|
||||
rollData.automatonPuissance = puissance
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
import { MournbladeUtility } from "../mournblade-utility.js"
|
||||
|
||||
/**
|
||||
* Dialogue de jet de dé pour Mournblade - Version DialogV2
|
||||
*/
|
||||
export class MournbladeRollDialog {
|
||||
|
||||
/**
|
||||
* Create and display the roll dialog
|
||||
* @param {MournbladeActor} actor - The actor making the roll
|
||||
* @param {Object} rollData - Data for the roll
|
||||
* @returns {Promise<MournbladeRollDialog>}
|
||||
*/
|
||||
static async create(actor, rollData) {
|
||||
// Préparer le contexte pour le template
|
||||
const context = {
|
||||
...rollData,
|
||||
difficulte: String(rollData.difficulte || 0),
|
||||
img: actor.img,
|
||||
name: actor.name,
|
||||
config: game.system.mournblade.config,
|
||||
}
|
||||
|
||||
// Si attrKey est "tochoose", préparer la liste des attributs sélectionnables
|
||||
if (rollData.attrKey === "tochoose") {
|
||||
context.selectableAttributes = actor.system.attributs
|
||||
}
|
||||
|
||||
// Rendre le template en HTML
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-mournblade/templates/roll-dialog-v2.hbs",
|
||||
context
|
||||
)
|
||||
|
||||
// Utiliser DialogV2.wait avec le HTML rendu
|
||||
return foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Test de Capacité", icon: "fa-solid fa-dice-d20" },
|
||||
classes: ["mournblade-roll-dialog"],
|
||||
position: { width: 500 },
|
||||
modal: false,
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
action: "rolld10",
|
||||
label: "Lancer 1d10",
|
||||
icon: "fa-solid fa-dice-d10",
|
||||
default: true,
|
||||
callback: async (event, button, dialog) => {
|
||||
this._updateRollDataFromForm(rollData, button.form.elements, actor)
|
||||
rollData.mainDice = "1d10"
|
||||
await MournbladeUtility.rollMournblade(rollData)
|
||||
}
|
||||
},
|
||||
{
|
||||
action: "rolld20",
|
||||
label: "Lancer 1d20",
|
||||
icon: "fa-solid fa-dice-d20",
|
||||
callback: async (event, button, dialog) => {
|
||||
this._updateRollDataFromForm(rollData, button.form.elements, actor)
|
||||
rollData.mainDice = "1d20"
|
||||
await MournbladeUtility.rollMournblade(rollData)
|
||||
}
|
||||
},
|
||||
],
|
||||
rejectClose: false,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and display the sortilège (multi-rune spell) roll dialog
|
||||
* @param {MournbladeActor} actor
|
||||
* @param {Object} rollData
|
||||
*/
|
||||
static async createSortilege(actor, rollData) {
|
||||
const ameDisponible = Math.max(1, (actor.system.ame.currentmax - actor.system.ame.value))
|
||||
const context = {
|
||||
...rollData,
|
||||
img: actor.img,
|
||||
name: actor.name,
|
||||
config: game.system.mournblade.config,
|
||||
runes: actor.getRunes(),
|
||||
runemode: "prononcer",
|
||||
ameDisponible,
|
||||
ameOptions: Array.from({ length: ameDisponible }, (_, i) => i + 1),
|
||||
modOptions: Array.from({ length: 21 }, (_, i) => i - 10),
|
||||
}
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-mournblade/templates/dialog-sortilege.hbs",
|
||||
context
|
||||
)
|
||||
|
||||
// Attach dynamic recalculation after render
|
||||
Hooks.once("renderDialogV2", (_app, html) => {
|
||||
const form = html.querySelector ? html : html[0]
|
||||
MournbladeRollDialog._attachSortilegeListeners(form)
|
||||
})
|
||||
|
||||
return foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Lancer un Sortilège", icon: "fa-solid fa-star-of-david" },
|
||||
classes: ["mournblade-roll-dialog"],
|
||||
position: { width: 520 },
|
||||
modal: false,
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
action: "rolld10",
|
||||
label: "Lancer 1d10",
|
||||
icon: "fa-solid fa-dice-d10",
|
||||
default: true,
|
||||
callback: async (event, button, dialog) => {
|
||||
this._updateSortilegeRollData(rollData, button.form.elements, actor)
|
||||
rollData.mainDice = "1d10"
|
||||
await MournbladeUtility.rollSortilege(rollData)
|
||||
}
|
||||
},
|
||||
{
|
||||
action: "rolld20",
|
||||
label: "Lancer 1d20",
|
||||
icon: "fa-solid fa-dice-d20",
|
||||
callback: async (event, button, dialog) => {
|
||||
this._updateSortilegeRollData(rollData, button.form.elements, actor)
|
||||
rollData.mainDice = "1d20"
|
||||
await MournbladeUtility.rollSortilege(rollData)
|
||||
}
|
||||
},
|
||||
],
|
||||
rejectClose: false,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the potion preparation dialog
|
||||
* @param {MournbladeActor} actor
|
||||
* @param {object} rollData
|
||||
*/
|
||||
static async createPotion(actor, rollData) {
|
||||
const ameDisponible = Math.max(1, (actor.system.ame.currentmax - actor.system.ame.value))
|
||||
const context = {
|
||||
...rollData,
|
||||
img: actor.img,
|
||||
name: actor.name,
|
||||
config: game.system.mournblade.config,
|
||||
runes: actor.getRunes(),
|
||||
ameDisponible,
|
||||
ameOptions: Array.from({ length: ameDisponible }, (_, i) => i + 1),
|
||||
modOptions: Array.from({ length: 21 }, (_, i) => i - 10),
|
||||
}
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-mournblade/templates/dialog-potion.hbs",
|
||||
context
|
||||
)
|
||||
|
||||
Hooks.once("renderDialogV2", (_app, html) => {
|
||||
const form = html.querySelector ? html : html[0]
|
||||
MournbladeRollDialog._attachPotionListeners(form)
|
||||
})
|
||||
|
||||
return foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Préparer une Potion", icon: "fa-solid fa-flask" },
|
||||
classes: ["mournblade-roll-dialog"],
|
||||
position: { width: 520 },
|
||||
modal: false,
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
action: "preparer",
|
||||
label: "Préparer",
|
||||
icon: "fa-solid fa-flask",
|
||||
default: true,
|
||||
callback: async (event, button, dialog) => {
|
||||
MournbladeRollDialog._updatePotionRollData(rollData, button.form.elements, actor)
|
||||
await MournbladeUtility.rollPotion(rollData)
|
||||
}
|
||||
},
|
||||
],
|
||||
rejectClose: false,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach dynamic recalculation listeners to the potion dialog
|
||||
* @param {HTMLElement} html
|
||||
*/
|
||||
static _attachPotionListeners(html) {
|
||||
const recalculate = () => {
|
||||
const radioChecked = html.querySelector('.potion-rune-radio:checked')
|
||||
const runeRow = radioChecked?.closest('[data-seuil]')
|
||||
const seuil = parseInt(runeRow?.dataset.seuil ?? 0)
|
||||
const pa = parseInt(html.querySelector('[name="pointsAme"]')?.value ?? 1)
|
||||
const mod = parseInt(html.querySelector('[name="modificateur"]')?.value ?? 0)
|
||||
|
||||
const difficulteEl = html.querySelector('#potion-difficulte')
|
||||
if (difficulteEl) difficulteEl.textContent = (seuil + pa) + (mod !== 0 ? ` (mod ${mod > 0 ? '+' : ''}${mod})` : '')
|
||||
|
||||
const tempsEl = html.querySelector('#potion-temps')
|
||||
if (tempsEl) {
|
||||
const heures = Math.max(1, Math.ceil(pa / 3))
|
||||
tempsEl.textContent = heures === 1 ? '1 heure' : `${heures} heures`
|
||||
}
|
||||
}
|
||||
|
||||
html.querySelectorAll('.potion-rune-radio, [name="pointsAme"]').forEach(el => {
|
||||
el.addEventListener('change', recalculate)
|
||||
})
|
||||
recalculate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update rollData from the potion dialog form elements
|
||||
* @param {object} rollData
|
||||
* @param {HTMLFormControlsCollection} formElements
|
||||
* @param {MournbladeActor} actor
|
||||
*/
|
||||
static _updatePotionRollData(rollData, formElements, actor) {
|
||||
const runeId = formElements['rune-selected']?.value
|
||||
if (runeId) {
|
||||
const rune = actor.getRunes().find(r => r._id === runeId)
|
||||
if (rune) {
|
||||
rollData.runeId = runeId
|
||||
rollData.runeName = rune.name
|
||||
rollData.runeImg = rune.img
|
||||
rollData.runeSeuil = rune.system.seuil ?? 0
|
||||
rollData.runeHautParler = rune.system.hautParler ?? ""
|
||||
}
|
||||
}
|
||||
rollData.pointsAme = parseInt(formElements['pointsAme']?.value ?? 1)
|
||||
rollData.forme = formElements['forme']?.value ?? "liquide"
|
||||
rollData.modificateur = parseInt(formElements['modificateur']?.value ?? 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach dynamic recalculation listeners to the sortilège dialog
|
||||
* @param {HTMLElement} html
|
||||
*/
|
||||
static _attachSortilegeListeners(html) {
|
||||
const recalculate = () => {
|
||||
const checkboxes = html.querySelectorAll('.sortilege-rune-checkbox:checked')
|
||||
const modeSelect = html.querySelector('[name="runemode"]')
|
||||
const mode = modeSelect?.value ?? "prononcer"
|
||||
|
||||
let maxSeuil = 0
|
||||
let totalAme = 0
|
||||
let totalActions = 0
|
||||
let count = 0
|
||||
|
||||
// Enable/disable points inputs based on checkbox state
|
||||
html.querySelectorAll('.sortilege-rune-checkbox').forEach(cb => {
|
||||
const row = cb.closest('.sortilege-rune-row')
|
||||
const pointsInput = row?.querySelector('.sortilege-rune-points')
|
||||
if (pointsInput) pointsInput.disabled = !cb.checked
|
||||
})
|
||||
|
||||
checkboxes.forEach(cb => {
|
||||
const row = cb.closest('.sortilege-rune-row')
|
||||
const seuil = Number(row?.dataset.seuil ?? 0)
|
||||
const pts = Number(row?.querySelector('.sortilege-rune-points')?.value ?? 1)
|
||||
if (seuil > maxSeuil) maxSeuil = seuil
|
||||
totalAme += pts
|
||||
totalActions += Math.ceil(pts / 3) * (mode === "inscrire" ? 2 : 1)
|
||||
count++
|
||||
})
|
||||
|
||||
const difficulte = count > 0 ? maxSeuil + (count - 1) : 0
|
||||
html.querySelector('#sortilege-difficulte').textContent = count > 0 ? difficulte : '—'
|
||||
html.querySelector('#sortilege-total-ame').textContent = totalAme
|
||||
html.querySelector('#sortilege-actions').textContent = totalActions
|
||||
}
|
||||
|
||||
html.querySelectorAll('.sortilege-rune-checkbox, .sortilege-rune-points, [name="runemode"]')
|
||||
.forEach(el => el.addEventListener('change', recalculate))
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract sortilège data from the form
|
||||
* @param {Object} rollData
|
||||
* @param {HTMLFormControlsCollection} formElements
|
||||
* @param {MournbladeActor} actor
|
||||
*/
|
||||
static _updateSortilegeRollData(rollData, formElements, actor) {
|
||||
rollData.runemode = formElements.runemode?.value ?? "prononcer"
|
||||
rollData.modificateur = Number(formElements.modificateur?.value ?? 0)
|
||||
rollData.runeautocible = formElements.runeautocible?.checked ?? false
|
||||
|
||||
// Collect selected runes with their soul points
|
||||
const runes = actor.getRunes()
|
||||
rollData.sortilegeRunes = []
|
||||
for (const rune of runes) {
|
||||
const checkbox = formElements[`rune-selected-${rune._id}`]
|
||||
if (checkbox?.checked) {
|
||||
const pts = Math.max(1, Number(formElements[`rune-points-${rune._id}`]?.value ?? 1))
|
||||
rollData.sortilegeRunes.push({
|
||||
id: rune._id,
|
||||
name: rune.name,
|
||||
img: rune.img,
|
||||
seuil: rune.system.seuil,
|
||||
formule: rune.system.formule,
|
||||
pts,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (rollData.sortilegeRunes.length === 0) return
|
||||
|
||||
const maxSeuil = Math.max(...rollData.sortilegeRunes.map(r => r.seuil))
|
||||
rollData.difficulte = maxSeuil + (rollData.sortilegeRunes.length - 1)
|
||||
rollData.runeame = rollData.sortilegeRunes.reduce((s, r) => s + r.pts, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} rollData - L'objet rollData à mettre à jour
|
||||
* @param {HTMLFormControlsCollection} formElements - Les éléments du formulaire
|
||||
* @param {MournbladeActor} actor - L'acteur pour récupérer les attributs
|
||||
* @private
|
||||
*/
|
||||
static _updateRollDataFromForm(rollData, formElements, actor) {
|
||||
// Attributs
|
||||
if (formElements.attrKey) {
|
||||
rollData.attrKey = formElements.attrKey.value
|
||||
if (rollData.attrKey !== "tochoose" && actor) {
|
||||
rollData.attr = foundry.utils.duplicate(actor.system.attributs[rollData.attrKey])
|
||||
rollData.actionImg = "systems/fvtt-mournblade/assets/icons/" + actor.system.attributs[rollData.attrKey].labelnorm + ".webp"
|
||||
}
|
||||
}
|
||||
|
||||
// Modificateurs de base
|
||||
if (formElements.difficulte) {
|
||||
rollData.difficulte = Number(formElements.difficulte.value)
|
||||
}
|
||||
if (formElements.modificateur) {
|
||||
rollData.modificateur = Number(formElements.modificateur.value)
|
||||
}
|
||||
|
||||
// Runes
|
||||
if (formElements.runemode) {
|
||||
rollData.runemode = String(formElements.runemode.value)
|
||||
}
|
||||
if (formElements.runeame) {
|
||||
rollData.runeame = Number(formElements.runeame.value)
|
||||
}
|
||||
if (formElements.runeautocible !== undefined) {
|
||||
rollData.runeautocible = formElements.runeautocible.checked
|
||||
}
|
||||
|
||||
// Combat mêlée
|
||||
if (formElements.typeAttaque) {
|
||||
rollData.typeAttaque = String(formElements.typeAttaque.value)
|
||||
}
|
||||
if (formElements.isMonte !== undefined) {
|
||||
rollData.isMonte = formElements.isMonte.checked
|
||||
}
|
||||
|
||||
// Combat distance
|
||||
if (formElements.visee !== undefined) {
|
||||
rollData.visee = formElements.visee.checked
|
||||
}
|
||||
if (formElements.cibleconsciente !== undefined) {
|
||||
rollData.cibleconsciente = formElements.cibleconsciente.checked
|
||||
}
|
||||
if (formElements.ciblecourt !== undefined) {
|
||||
rollData.ciblecourt = formElements.ciblecourt.checked
|
||||
}
|
||||
if (formElements.typeCouvert) {
|
||||
rollData.typeCouvert = String(formElements.typeCouvert.value)
|
||||
}
|
||||
|
||||
// Désavantages
|
||||
if (!rollData.desavantages) rollData.desavantages = {}
|
||||
|
||||
if (formElements.cibleausol !== undefined) {
|
||||
rollData.desavantages.cibleausol = formElements.cibleausol.checked
|
||||
}
|
||||
if (formElements.cibledesarmee !== undefined) {
|
||||
rollData.desavantages.cibledesarmee = formElements.cibledesarmee.checked
|
||||
}
|
||||
if (formElements.ciblerestreint !== undefined) {
|
||||
rollData.desavantages.ciblerestreint = formElements.ciblerestreint.checked
|
||||
}
|
||||
if (formElements.cibleimmobilisée !== undefined) {
|
||||
rollData.desavantages.cibleimmobilisée = formElements.cibleimmobilisée.checked
|
||||
}
|
||||
if (formElements.ciblesurplomb !== undefined) {
|
||||
rollData.desavantages.ciblesurplomb = formElements.ciblesurplomb.checked
|
||||
}
|
||||
|
||||
// Double D20
|
||||
if (formElements.doubleD20 !== undefined) {
|
||||
rollData.doubleD20 = formElements.doubleD20.checked
|
||||
}
|
||||
|
||||
// Modifiers
|
||||
if (rollData.modifiers) {
|
||||
rollData.modifiers.forEach((modifier, idx) => {
|
||||
const checkbox = formElements[`apply-modifier-${idx}`]
|
||||
if (checkbox) {
|
||||
modifier.system.apply = checkbox.checked
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Index des applications AppV2 pour Mournblade
|
||||
* Ce fichier centralise tous les exports des applications
|
||||
*/
|
||||
|
||||
// Applications de feuilles d'acteurs
|
||||
export { default as MournbladePersonnageSheet } from './mournblade-personnage-sheet.mjs';
|
||||
export { default as MournbladeCreatureSheet } from './mournblade-creature-sheet.mjs';
|
||||
export { default as MournbladePnjSheet } from './mournblade-pnj-sheet.mjs';
|
||||
|
||||
// Applications de feuilles d'items
|
||||
export { default as MournbladeArmeSheet } from './mournblade-arme-sheet.mjs';
|
||||
export { default as MournbladeBouclierSheet } from './mournblade-bouclier-sheet.mjs';
|
||||
export { default as MournbladeCapaciteSheet } from './mournblade-capacite-sheet.mjs';
|
||||
export { default as MournbladeCompetenceSheet } from './mournblade-competence-sheet.mjs';
|
||||
export { default as MournbladeDonSheet } from './mournblade-don-sheet.mjs';
|
||||
export { default as MournbladeEquipementSheet } from './mournblade-equipement-sheet.mjs';
|
||||
export { default as MournbladeHeritageSheet } from './mournblade-heritage-sheet.mjs';
|
||||
export { default as MournbladeMetierSheet } from './mournblade-metier-sheet.mjs';
|
||||
export { default as MournbladeModifierSheet } from './mournblade-modifier-sheet.mjs';
|
||||
export { default as MournbladeMonnaieSheet } from './mournblade-monnaie-sheet.mjs';
|
||||
export { default as MournbladeOrigineSheet } from './mournblade-origine-sheet.mjs';
|
||||
export { default as MournbladePacteSheet } from './mournblade-pacte-sheet.mjs';
|
||||
export { default as MournbladeProtectionSheet } from './mournblade-protection-sheet.mjs';
|
||||
export { default as MournbladeRuneSheet } from './mournblade-rune-sheet.mjs';
|
||||
export { default as MournbladeRuneEffectSheet } from './mournblade-runeeffect-sheet.mjs';
|
||||
export { default as MournbladePotionSheet } from './mournblade-potion-sheet.mjs';
|
||||
export { default as MournbladeTendanceSheet } from './mournblade-tendance-sheet.mjs';
|
||||
export { default as MournbladeTraitChaotiqueSheet } from './mournblade-traitchaotique-sheet.mjs';
|
||||
export { default as MournbladeTraitEspeceSheet } from './mournblade-traitespece-sheet.mjs';
|
||||
@@ -0,0 +1,522 @@
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api
|
||||
|
||||
import { MournbladeUtility } from "../../mournblade-utility.js"
|
||||
|
||||
export default class MournbladeActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) {
|
||||
/**
|
||||
* Different sheet modes.
|
||||
* @enum {number}
|
||||
*/
|
||||
static SHEET_MODES = { EDIT: 0, PLAY: 1 }
|
||||
|
||||
constructor(options = {}) {
|
||||
super(options)
|
||||
this.#dragDrop = this.#createDragDropHandlers()
|
||||
this._sheetMode = this.constructor.SHEET_MODES.PLAY // Commencer en mode visualisation
|
||||
}
|
||||
|
||||
#dragDrop
|
||||
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["fvtt-mournblade", "sheet", "actor"],
|
||||
position: {
|
||||
width: 650,
|
||||
height: 720,
|
||||
},
|
||||
form: {
|
||||
submitOnChange: true,
|
||||
closeOnSubmit: false,
|
||||
},
|
||||
window: {
|
||||
resizable: true,
|
||||
},
|
||||
tabs: [
|
||||
{
|
||||
navSelector: 'nav[data-group="primary"]',
|
||||
contentSelector: "section.sheet-body",
|
||||
initial: "stats",
|
||||
},
|
||||
],
|
||||
dragDrop: [{ dragSelector: ".item-list .item", dropSelector: null }],
|
||||
actions: {
|
||||
editImage: MournbladeActorSheet.#onEditImage,
|
||||
toggleSheet: MournbladeActorSheet.#onToggleSheet,
|
||||
editItem: MournbladeActorSheet.#onEditItem,
|
||||
deleteItem: MournbladeActorSheet.#onDeleteItem,
|
||||
createItem: MournbladeActorSheet.#onCreateItem,
|
||||
postItem: MournbladeActorSheet.#onPostItem,
|
||||
equipItem: MournbladeActorSheet.#onEquipItem,
|
||||
modifyQuantity: MournbladeActorSheet.#onModifyQuantity,
|
||||
modifySante: MournbladeActorSheet.#onModifySante,
|
||||
modifyAme: MournbladeActorSheet.#onModifyAme,
|
||||
rollAttribut: MournbladeActorSheet.#onRollAttribut,
|
||||
rollCompetence: MournbladeActorSheet.#onRollCompetence,
|
||||
rollRune: MournbladeActorSheet.#onRollRune,
|
||||
rollSortilege: MournbladeActorSheet.#onRollSortilege,
|
||||
preparePotion: MournbladeActorSheet.#onPreparePotion,
|
||||
invoquerElementaire: MournbladeActorSheet.#onInvoquerElementaire,
|
||||
bannirElementaire: MournbladeActorSheet.#onBannirElementaire,
|
||||
invoquerDemon: MournbladeActorSheet.#onInvoquerDemon,
|
||||
libererDemon: MournbladeActorSheet.#onLibererDemon,
|
||||
invoquerEsprit: MournbladeActorSheet.#onInvoquerEsprit,
|
||||
enchanter: MournbladeActorSheet.#onEnchanter,
|
||||
rollArmeOffensif: MournbladeActorSheet.#onRollArmeOffensif,
|
||||
rollArmeSpecial: MournbladeActorSheet.#onRollArmeSpecial,
|
||||
rollArmeDegats: MournbladeActorSheet.#onRollArmeDegats,
|
||||
rollAssommer: MournbladeActorSheet.#onRollAssommer,
|
||||
rollImmobiliser: MournbladeActorSheet.#onRollImmobiliser,
|
||||
rollFuir: MournbladeActorSheet.#onRollFuir,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the sheet currently in 'Play' mode?
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isPlayMode() {
|
||||
if (this._sheetMode === undefined) this._sheetMode = this.constructor.SHEET_MODES.PLAY
|
||||
return this._sheetMode === this.constructor.SHEET_MODES.PLAY
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the sheet currently in 'Edit' mode?
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isEditMode() {
|
||||
if (this._sheetMode === undefined) this._sheetMode = this.constructor.SHEET_MODES.PLAY
|
||||
return this._sheetMode === this.constructor.SHEET_MODES.EDIT
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab groups state
|
||||
* @type {object}
|
||||
*/
|
||||
tabGroups = { primary: "stats" }
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const actor = this.document
|
||||
|
||||
const context = {
|
||||
actor: actor,
|
||||
system: actor.system,
|
||||
source: actor.toObject(),
|
||||
fields: actor.schema.fields,
|
||||
systemFields: actor.system.schema.fields,
|
||||
isEditable: this.isEditable,
|
||||
isEditMode: this.isEditMode,
|
||||
isPlayMode: this.isPlayMode,
|
||||
isGM: game.user.isGM,
|
||||
config: game.system.mournblade.config,
|
||||
enrichedDescription: await foundry.applications.ux.TextEditor.implementation.enrichHTML(actor.system.biodata?.description || "", { async: true }),
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_preSyncPartState(partId, newElement, priorElement, state) {
|
||||
super._preSyncPartState(partId, newElement, priorElement, state)
|
||||
// Save scrollable tab positions for deferred restoration in _onRender.
|
||||
// Tabs are hidden (display:none) at _syncPartState time, so scrollTop
|
||||
// assignments have no effect. We re-apply them after making tabs visible.
|
||||
const part = this.constructor.PARTS?.[partId]
|
||||
if (part?.scrollable) {
|
||||
this._pendingScrollRestores = part.scrollable.map(selector => {
|
||||
const el = selector ? priorElement.querySelector(selector) : priorElement
|
||||
return el ? { selector, scrollTop: el.scrollTop } : null
|
||||
}).filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_onRender(context, options) {
|
||||
super._onRender(context, options)
|
||||
this.#dragDrop.forEach((d) => d.bind(this.element))
|
||||
|
||||
// Handle edit-item-data changes
|
||||
this.element.querySelectorAll('.edit-item-data').forEach(element => {
|
||||
element.addEventListener('change', async (event) => {
|
||||
const target = event.currentTarget
|
||||
const itemElement = target.closest('[data-item-id]')
|
||||
if (!itemElement) return
|
||||
|
||||
const itemId = itemElement.dataset.itemId
|
||||
const itemType = itemElement.dataset.itemType
|
||||
const itemField = target.dataset.itemField
|
||||
const dataType = target.dataset.dtype
|
||||
const value = target.value
|
||||
|
||||
await this.document.editItemField(itemId, itemType, itemField, dataType, value)
|
||||
})
|
||||
})
|
||||
|
||||
// Activate tab navigation manually
|
||||
const nav = this.element.querySelector('nav.tabs[data-group]')
|
||||
if (nav) {
|
||||
const group = nav.dataset.group
|
||||
const activeTab = this.tabGroups[group] || "stats"
|
||||
|
||||
const switchTab = (tab) => {
|
||||
this.tabGroups[group] = tab
|
||||
nav.querySelectorAll('[data-tab]').forEach(link => {
|
||||
link.classList.toggle('active', link.dataset.tab === tab)
|
||||
})
|
||||
this.element.querySelectorAll('[data-group="' + group + '"][data-tab]').forEach(content => {
|
||||
content.classList.toggle('active', content.dataset.tab === tab)
|
||||
})
|
||||
}
|
||||
|
||||
// Set initial state (makes active tab visible)
|
||||
switchTab(activeTab)
|
||||
|
||||
// Restore scroll positions now that the active tab is visible
|
||||
if (this._pendingScrollRestores?.length) {
|
||||
for (const { selector, scrollTop } of this._pendingScrollRestores) {
|
||||
const el = selector ? this.element.querySelector(selector) : this.element
|
||||
if (el) el.scrollTop = scrollTop
|
||||
}
|
||||
this._pendingScrollRestores = null
|
||||
}
|
||||
|
||||
// Tab clicks: DOM-only, no re-render (preserves scroll positions)
|
||||
nav.querySelectorAll('[data-tab]').forEach(link => {
|
||||
link.addEventListener('click', (event) => {
|
||||
event.preventDefault()
|
||||
switchTab(link.dataset.tab)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// #region Drag-and-Drop Workflow
|
||||
/**
|
||||
* Create drag-and-drop workflow handlers for this Application
|
||||
*/
|
||||
#createDragDropHandlers() {
|
||||
return []
|
||||
}
|
||||
|
||||
// #region Actions
|
||||
/** @override */
|
||||
static ACTIONS = {
|
||||
editImage: MournbladeActorSheet.#onEditImage,
|
||||
toggleSheet: MournbladeActorSheet.#onToggleSheet,
|
||||
editItem: MournbladeActorSheet.#onEditItem,
|
||||
deleteItem: MournbladeActorSheet.#onDeleteItem,
|
||||
createItem: MournbladeActorSheet.#onCreateItem,
|
||||
equipItem: MournbladeActorSheet.#onEquipItem,
|
||||
modifyQuantity: MournbladeActorSheet.#onModifyQuantity,
|
||||
rollAttribut: MournbladeActorSheet.#onRollAttribut,
|
||||
rollCompetence: MournbladeActorSheet.#onRollCompetence,
|
||||
rollArmeOffensif: MournbladeActorSheet.#onRollArmeOffensif,
|
||||
rollArmeDegats: MournbladeActorSheet.#onRollArmeDegats,
|
||||
rollAssommer: MournbladeActorSheet.#onRollAssommer,
|
||||
rollImmobiliser: MournbladeActorSheet.#onRollImmobiliser,
|
||||
rollFuir: MournbladeActorSheet.#onRollFuir,
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle editing the actor image
|
||||
* @param {Event} event - The triggering event
|
||||
*/
|
||||
static async #onEditImage(event) {
|
||||
event.preventDefault()
|
||||
const sheet = this
|
||||
const filePicker = new FilePicker({
|
||||
type: "image",
|
||||
current: sheet.document.img,
|
||||
callback: (path) => {
|
||||
sheet.document.update({ img: path })
|
||||
},
|
||||
})
|
||||
filePicker.browse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle toggling the sheet mode
|
||||
* @param {Event} event - The triggering event
|
||||
*/
|
||||
static async #onToggleSheet(event) {
|
||||
event.preventDefault()
|
||||
const sheet = this
|
||||
sheet._sheetMode = sheet._sheetMode === sheet.constructor.SHEET_MODES.PLAY ? sheet.constructor.SHEET_MODES.EDIT : sheet.constructor.SHEET_MODES.PLAY
|
||||
sheet.render()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle editing an item
|
||||
* @param {Event} event - The triggering event
|
||||
* @param {HTMLElement} target - The target element
|
||||
*/
|
||||
static async #onEditItem(event, target) {
|
||||
const li = target.closest(".item")
|
||||
const itemId = li?.dataset.itemId
|
||||
if (!itemId) return
|
||||
const item = this.actor.items.get(itemId)
|
||||
if (item) item.sheet.render(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle deleting an item
|
||||
* @param {Event} event - The triggering event
|
||||
* @param {HTMLElement} target - The target element
|
||||
*/
|
||||
static async #onDeleteItem(event, target) {
|
||||
const li = target.closest(".item")
|
||||
await MournbladeUtility.confirmDelete(this, li)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle creating an item
|
||||
* @param {Event} event - The triggering event
|
||||
* @param {HTMLElement} target - The target element
|
||||
*/
|
||||
static async #onCreateItem(event, target) {
|
||||
const itemType = target.dataset.type
|
||||
await this.actor.createEmbeddedDocuments("Item", [{ name: `Nouveau ${itemType}`, type: itemType }], { renderSheet: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle posting an item to chat
|
||||
*/
|
||||
static async #onPostItem(event, target) {
|
||||
const li = target.closest(".item")
|
||||
const itemId = li?.dataset.itemId ?? target.dataset.itemId
|
||||
if (!itemId) return
|
||||
MournbladeUtility.postItemToChat(this.actor.id, itemId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle equipping an item
|
||||
* @param {Event} event - The triggering event
|
||||
* @param {HTMLElement} target - The target element
|
||||
*/
|
||||
static async #onEquipItem(event, target) {
|
||||
const li = target.closest(".item")
|
||||
const itemId = li?.dataset.itemId
|
||||
if (!itemId) return
|
||||
const item = this.actor.items.get(itemId)
|
||||
if (item) {
|
||||
await item.update({ "system.equipped": !item.system.equipped })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle modifying the quantity of an item
|
||||
* @param {Event} event - The triggering event
|
||||
* @param {HTMLElement} target - The target element
|
||||
*/
|
||||
static async #onModifyQuantity(event, target) {
|
||||
const li = target.closest('[data-item-id]')
|
||||
const itemId = li?.dataset.itemId
|
||||
const value = Number.parseInt(target.dataset.quantiteValue)
|
||||
const item = this.document.items.get(itemId)
|
||||
if (item) {
|
||||
const newQuantity = Math.max(0, (item.system.quantite || 0) + value)
|
||||
await item.update({ "system.quantite": newQuantity })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle modifying santé (health) values
|
||||
* @param {Event} event - The triggering event
|
||||
* @param {HTMLElement} target - The target element
|
||||
*/
|
||||
static async #onModifySante(event, target) {
|
||||
const type = target.dataset.type
|
||||
const value = Number.parseInt(target.dataset.value)
|
||||
const actor = this.document
|
||||
const currentValue = actor.system.sante[type] || 0
|
||||
const newValue = Math.max(0, currentValue + value)
|
||||
await actor.update({ [`system.sante.${type}`]: newValue })
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle modifying âme (soul) value
|
||||
* @param {Event} event - The triggering event
|
||||
* @param {HTMLElement} target - The target element
|
||||
*/
|
||||
static async #onModifyAme(event, target) {
|
||||
const value = Number.parseInt(target.dataset.value)
|
||||
const actor = this.document
|
||||
const currentValue = actor.system.ame.value || 0
|
||||
const newValue = Math.max(0, currentValue + value)
|
||||
await actor.update({ "system.ame.value": newValue })
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle rolling an attribut
|
||||
* @param {Event} event - The triggering event
|
||||
*/
|
||||
static async #onRollAttribut(event, target) {
|
||||
event.preventDefault()
|
||||
const sheet = this
|
||||
const attrKey = target.dataset.attrKey
|
||||
const actor = sheet.document
|
||||
await actor.rollAttribut(attrKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle rolling a competence
|
||||
* @param {Event} event - The triggering event
|
||||
*/
|
||||
static async #onRollCompetence(event, target) {
|
||||
event.preventDefault()
|
||||
const sheet = this
|
||||
const attrKey = target.dataset.attrKey
|
||||
const li = target.closest('[data-item-id]')
|
||||
const compId = li?.dataset.itemId
|
||||
const actor = sheet.document
|
||||
await actor.rollCompetence(attrKey, compId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle rolling a rune
|
||||
* @param {Event} event - The triggering event
|
||||
*/
|
||||
static async #onRollRune(event, target) {
|
||||
event.preventDefault()
|
||||
const sheet = this
|
||||
const li = target.closest('[data-item-id]')
|
||||
const runeId = li?.dataset.itemId
|
||||
const actor = sheet.document
|
||||
await actor.rollRune(runeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle launching a sortilège (multi-rune spell)
|
||||
*/
|
||||
static async #onRollSortilege(event, target) {
|
||||
event.preventDefault()
|
||||
await this.document.rollSortilege()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle preparing a potion
|
||||
*/
|
||||
static async #onPreparePotion(event, target) {
|
||||
event.preventDefault()
|
||||
await this.document.preparePotion()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle invoking an elemental
|
||||
*/
|
||||
static async #onInvoquerElementaire(event, target) {
|
||||
event.preventDefault()
|
||||
await this.document.invoquerElementaire()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle banishing an elemental invocation
|
||||
*/
|
||||
static async #onBannirElementaire(event, target) {
|
||||
event.preventDefault()
|
||||
const invocIndex = parseInt(target.dataset.invocIndex ?? "0")
|
||||
await this.document.bannirElementaire(invocIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle invoking a demon
|
||||
*/
|
||||
static async #onInvoquerDemon(event, target) {
|
||||
event.preventDefault()
|
||||
await this.document.invoquerDemon()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle releasing a demon invocation
|
||||
*/
|
||||
static async #onLibererDemon(event, target) {
|
||||
event.preventDefault()
|
||||
const invocIndex = parseInt(target.dataset.invocIndex ?? "0")
|
||||
await this.document.libererDemon(invocIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle invoking a Law Spirit into an Automaton
|
||||
*/
|
||||
static async #onInvoquerEsprit(event, target) {
|
||||
event.preventDefault()
|
||||
await this.document.invoquerEspritLoi()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle enchanting an item with Loi power
|
||||
*/
|
||||
static async #onEnchanter(event, target) {
|
||||
event.preventDefault()
|
||||
const itemId = target.dataset.itemId
|
||||
await this.document.enchanter(itemId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle rolling an arme offensif
|
||||
* @param {Event} event - The triggering event
|
||||
*/
|
||||
static async #onRollArmeOffensif(event, target) {
|
||||
event.preventDefault()
|
||||
const sheet = this
|
||||
const armeId = target.dataset.armeId
|
||||
const actor = sheet.document
|
||||
await actor.rollArmeOffensif(armeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle rolling an arme degats
|
||||
* @param {Event} event - The triggering event
|
||||
*/
|
||||
static async #onRollArmeDegats(event, target) {
|
||||
event.preventDefault()
|
||||
const sheet = this
|
||||
const armeId = target.dataset.armeId
|
||||
const actor = sheet.document
|
||||
await actor.rollArmeDegats(armeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle rolling an arme special
|
||||
* @param {Event} event - The triggering event
|
||||
*/
|
||||
static async #onRollArmeSpecial(event, target) {
|
||||
event.preventDefault()
|
||||
const sheet = this
|
||||
const armeId = target.dataset.armeId
|
||||
const actor = sheet.document
|
||||
await actor.rollArmeSpecial(armeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle rolling an assommer
|
||||
* @param {Event} event - The triggering event
|
||||
*/
|
||||
static async #onRollAssommer(event, target) {
|
||||
event.preventDefault()
|
||||
const sheet = this
|
||||
const actor = sheet.document
|
||||
await actor.rollAssomer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle rolling an immobiliser
|
||||
* @param {Event} event - The triggering event
|
||||
*/
|
||||
static async #onRollImmobiliser(event, target) {
|
||||
event.preventDefault()
|
||||
const sheet = this
|
||||
const actor = sheet.document
|
||||
await actor.rollImmobiliser()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle rolling a fuir
|
||||
* @param {Event} event - The triggering event
|
||||
*/
|
||||
static async #onRollFuir(event, target) {
|
||||
event.preventDefault()
|
||||
const sheet = this
|
||||
const actor = sheet.document
|
||||
await actor.rollFuir()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api
|
||||
|
||||
export default class MournbladeItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) {
|
||||
constructor(options = {}) {
|
||||
super(options)
|
||||
this.#dragDrop = this.#createDragDropHandlers()
|
||||
}
|
||||
|
||||
#dragDrop
|
||||
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["fvtt-mournblade", "item"],
|
||||
position: {
|
||||
width: 620,
|
||||
height: 600,
|
||||
},
|
||||
form: {
|
||||
submitOnChange: true,
|
||||
},
|
||||
window: {
|
||||
resizable: true,
|
||||
},
|
||||
tabs: [
|
||||
{
|
||||
navSelector: 'nav[data-group="primary"]',
|
||||
contentSelector: "section.sheet-body",
|
||||
initial: "description",
|
||||
},
|
||||
],
|
||||
dragDrop: [{ dragSelector: "[data-drag]", dropSelector: null }],
|
||||
actions: {
|
||||
editImage: MournbladeItemSheet.#onEditImage,
|
||||
postItem: MournbladeItemSheet.#onPostItem,
|
||||
enchanter: MournbladeItemSheet.#onEnchanter,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab groups state
|
||||
* @type {object}
|
||||
*/
|
||||
tabGroups = { primary: "description" }
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const item = this.document
|
||||
const enchantableTypes = ["arme", "equipement", "protection", "bouclier"]
|
||||
const actorIsLoyal = item.actor?.getAlignement?.() === "loyal"
|
||||
const alreadyEnchanted = enchantableTypes.includes(item.type) && (item.system.enchantementLoi?.actif ?? false)
|
||||
const canEnchant = enchantableTypes.includes(item.type) && !!item.actor && actorIsLoyal && !alreadyEnchanted
|
||||
const context = {
|
||||
fields: item.schema.fields,
|
||||
systemFields: item.system.schema.fields,
|
||||
item,
|
||||
system: item.system,
|
||||
source: item.toObject(),
|
||||
enrichedDescription: await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.system.description, { async: true }),
|
||||
isEditMode: true,
|
||||
isEditable: this.isEditable,
|
||||
isGM: game.user.isGM,
|
||||
config: game.system.mournblade.config,
|
||||
canEnchant,
|
||||
enchantementActif: alreadyEnchanted,
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_onRender(context, options) {
|
||||
super._onRender(context, options)
|
||||
this.#dragDrop.forEach((d) => d.bind(this.element))
|
||||
|
||||
// Activate tab navigation manually
|
||||
const nav = this.element.querySelector('nav.tabs[data-group]')
|
||||
if (nav) {
|
||||
const group = nav.dataset.group
|
||||
// Activate the current tab
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
// Show/hide tab content
|
||||
this.element.querySelectorAll('[data-group="' + group + '"][data-tab]').forEach(content => {
|
||||
content.classList.toggle('active', content.dataset.tab === activeTab)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// #region Drag-and-Drop Workflow
|
||||
/**
|
||||
* Create drag-and-drop workflow handlers for this Application
|
||||
*/
|
||||
#createDragDropHandlers() {
|
||||
return []
|
||||
}
|
||||
|
||||
// #region Actions
|
||||
|
||||
/**
|
||||
* Handle enchanting this item with Loi power
|
||||
*/
|
||||
static async #onEnchanter(event) {
|
||||
event.preventDefault()
|
||||
const item = this.document
|
||||
if (!item.actor) {
|
||||
ui.notifications.warn("Cet objet doit être sur un personnage pour être enchanté.")
|
||||
return
|
||||
}
|
||||
await item.actor.enchanter(item.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle editing the item image
|
||||
* @param {Event} event - The triggering event
|
||||
*/
|
||||
static async #onEditImage(event) {
|
||||
event.preventDefault()
|
||||
const filePicker = new FilePicker({
|
||||
type: "image",
|
||||
current: this.document.img,
|
||||
callback: (path) => {
|
||||
this.document.update({ img: path })
|
||||
},
|
||||
})
|
||||
filePicker.browse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle posting the item to chat
|
||||
* @param {Event} event - The triggering event
|
||||
*/
|
||||
static async #onPostItem(event) {
|
||||
event.preventDefault()
|
||||
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,
|
||||
})
|
||||
|
||||
// Localized type label
|
||||
const typeLabels = {
|
||||
arme: "Arme", bouclier: "Bouclier", competence: "Compétence",
|
||||
rune: "Rune", runeeffect: "Rune Active", don: "Don", pacte: "Pacte",
|
||||
protection: "Protection", equipement: "Équipement", heritage: "Héritage",
|
||||
metier: "Métier", capacite: "Capacité", tendance: "Tendance",
|
||||
traitchaotique: "Trait Chaotique", traitespece: "Trait d'Espèce",
|
||||
origine: "Origine", modifier: "Modificateur", monnaie: "Monnaie",
|
||||
potion: "Potion"
|
||||
}
|
||||
chatData.typeLabel = typeLabels[chatData.type] ?? chatData.type
|
||||
|
||||
// Type icon for the badge
|
||||
const typeIcons = {
|
||||
arme: "fa-sword", bouclier: "fa-shield-halved", competence: "fa-graduation-cap",
|
||||
rune: "fa-star-of-david", runeeffect: "fa-star-of-david", don: "fa-hand-sparkles",
|
||||
pacte: "fa-scroll", protection: "fa-shield", equipement: "fa-box",
|
||||
heritage: "fa-dna", metier: "fa-hammer", capacite: "fa-bolt",
|
||||
tendance: "fa-yin-yang", traitchaotique: "fa-skull", traitespece: "fa-paw",
|
||||
origine: "fa-compass", modifier: "fa-sliders", monnaie: "fa-coins",
|
||||
potion: "fa-flask"
|
||||
}
|
||||
chatData.typeIcon = typeIcons[chatData.type] ?? "fa-cube"
|
||||
|
||||
// Potion: add localized labels for statut and forme
|
||||
if (chatData.type === "potion") {
|
||||
const statutLabels = {
|
||||
inconnue: game.i18n.localize("MNBL.potionInconnue"),
|
||||
efficace: game.i18n.localize("MNBL.potionEfficace"),
|
||||
heroique: game.i18n.localize("MNBL.potionHeroique"),
|
||||
inefficace: game.i18n.localize("MNBL.potionInefficace"),
|
||||
poison: game.i18n.localize("MNBL.potionPoison"),
|
||||
}
|
||||
const formeLabels = {
|
||||
liquide: game.i18n.localize("MNBL.potionLiquide"),
|
||||
onguent: game.i18n.localize("MNBL.potionOnguent"),
|
||||
cachets: game.i18n.localize("MNBL.potionCachets"),
|
||||
pilules: game.i18n.localize("MNBL.potionPilules"),
|
||||
}
|
||||
chatData.system.statutLabel = statutLabels[chatData.system.statut] ?? chatData.system.statut
|
||||
chatData.system.formeLabel = formeLabels[chatData.system.forme] ?? chatData.system.forme
|
||||
}
|
||||
|
||||
const html = await foundry.applications.handlebars.renderTemplate('systems/fvtt-mournblade/templates/post-item.hbs', chatData)
|
||||
ChatMessage.create({ user: game.user.id, content: html })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import MournbladeItemSheet from "./base-item-sheet.mjs"
|
||||
|
||||
export default class MournbladeArmeSheet extends MournbladeItemSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["arme"],
|
||||
position: {
|
||||
width: 620,
|
||||
},
|
||||
window: {
|
||||
contentClasses: ["arme-content"],
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: "systems/fvtt-mournblade/templates/item-arme-sheet.hbs",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "description",
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an array of form header tabs.
|
||||
* @returns {Record<string, Partial<ApplicationTab>>}
|
||||
*/
|
||||
#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
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import MournbladeItemSheet from "./base-item-sheet.mjs"
|
||||
|
||||
export default class MournbladeBouclierSheet extends MournbladeItemSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["bouclier"],
|
||||
position: {
|
||||
width: 620,
|
||||
},
|
||||
window: {
|
||||
contentClasses: ["bouclier-content"],
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: "systems/fvtt-mournblade/templates/item-bouclier-sheet.hbs",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "description",
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an array of form header tabs.
|
||||
* @returns {Record<string, Partial<ApplicationTab>>}
|
||||
*/
|
||||
#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
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import MournbladeItemSheet from "./base-item-sheet.mjs"
|
||||
|
||||
export default class MournbladeCapaciteSheet extends MournbladeItemSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["capacite"],
|
||||
position: {
|
||||
width: 620,
|
||||
},
|
||||
window: {
|
||||
contentClasses: ["capacite-content"],
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: "systems/fvtt-mournblade/templates/item-capacite-sheet.hbs",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "description",
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an array of form header tabs.
|
||||
* @returns {Record<string, Partial<ApplicationTab>>}
|
||||
*/
|
||||
#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
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import MournbladeItemSheet from "./base-item-sheet.mjs"
|
||||
|
||||
export default class MournbladeCompetenceSheet extends MournbladeItemSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["competence"],
|
||||
position: {
|
||||
width: 620,
|
||||
},
|
||||
window: {
|
||||
contentClasses: ["competence-content"],
|
||||
},
|
||||
actions: {
|
||||
...MournbladeItemSheet.DEFAULT_OPTIONS.actions,
|
||||
addPredilection: MournbladeCompetenceSheet.#onAddPredilection,
|
||||
deletePredilection: MournbladeCompetenceSheet.#onDeletePredilection,
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: "systems/fvtt-mournblade/templates/item-competence-sheet.hbs",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "description",
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an array of form header tabs.
|
||||
* @returns {Record<string, Partial<ApplicationTab>>}
|
||||
*/
|
||||
#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
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
return context
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new empty predilection to the competence
|
||||
* @param {PointerEvent} event
|
||||
*/
|
||||
static async #onAddPredilection(event) {
|
||||
event.preventDefault()
|
||||
const predilections = foundry.utils.duplicate(this.document.system.predilections ?? [])
|
||||
predilections.push({ name: "", used: false })
|
||||
await this.document.update({ "system.predilections": predilections })
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a predilection by index
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async #onDeletePredilection(event, target) {
|
||||
event.preventDefault()
|
||||
const idx = Number(target.dataset.predictionIndex)
|
||||
const predilections = foundry.utils.duplicate(this.document.system.predilections ?? [])
|
||||
predilections.splice(idx, 1)
|
||||
await this.document.update({ "system.predilections": predilections })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import MournbladeActorSheet from "./base-actor-sheet.mjs"
|
||||
|
||||
export default class MournbladeCreatureSheet extends MournbladeActorSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
...super.DEFAULT_OPTIONS,
|
||||
classes: [...super.DEFAULT_OPTIONS.classes],
|
||||
window: {
|
||||
...super.DEFAULT_OPTIONS.window,
|
||||
title: "SHEETS.Actor.creature",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
sheet: {
|
||||
template: "systems/fvtt-mournblade/templates/creature-sheet.hbs",
|
||||
scrollable: [".tab.competences", ".tab.equipement", ".tab.biodata"],
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "stats",
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
const actor = this.document
|
||||
|
||||
// Add creature-specific data
|
||||
context.skills = actor.getSkills()
|
||||
context.armes = foundry.utils.duplicate(actor.getWeapons())
|
||||
context.protections = foundry.utils.duplicate(actor.getArmors())
|
||||
context.capacites = foundry.utils.duplicate(actor.getCapacites())
|
||||
context.runes = foundry.utils.duplicate(actor.getRunes())
|
||||
context.combat = actor.getCombatValues()
|
||||
context.equipements = foundry.utils.duplicate(actor.getEquipments())
|
||||
context.protectionTotal = actor.getProtectionTotal()
|
||||
context.santeMalus = actor.getStatusMalus()
|
||||
context.ameMalus = actor.getAmeMalus()
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import MournbladeItemSheet from "./base-item-sheet.mjs"
|
||||
|
||||
export default class MournbladeDonSheet extends MournbladeItemSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["don"],
|
||||
position: {
|
||||
width: 620,
|
||||
},
|
||||
window: {
|
||||
contentClasses: ["don-content"],
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: "systems/fvtt-mournblade/templates/item-don-sheet.hbs",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "description",
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an array of form header tabs.
|
||||
* @returns {Record<string, Partial<ApplicationTab>>}
|
||||
*/
|
||||
#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
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
context.enrichedSacrifice = await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.sacrifice, { async: true })
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import MournbladeItemSheet from "./base-item-sheet.mjs"
|
||||
|
||||
export default class MournbladeEquipementSheet extends MournbladeItemSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["equipement"],
|
||||
position: {
|
||||
width: 620,
|
||||
},
|
||||
window: {
|
||||
contentClasses: ["equipement-content"],
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: "systems/fvtt-mournblade/templates/item-equipement-sheet.hbs",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "description",
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an array of form header tabs.
|
||||
* @returns {Record<string, Partial<ApplicationTab>>}
|
||||
*/
|
||||
#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
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import MournbladeItemSheet from "./base-item-sheet.mjs"
|
||||
|
||||
export default class MournbladeHeritageSheet extends MournbladeItemSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["heritage"],
|
||||
position: {
|
||||
width: 620,
|
||||
},
|
||||
window: {
|
||||
contentClasses: ["heritage-content"],
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: "systems/fvtt-mournblade/templates/item-heritage-sheet.hbs",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "description",
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an array of form header tabs.
|
||||
* @returns {Record<string, Partial<ApplicationTab>>}
|
||||
*/
|
||||
#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
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import MournbladeItemSheet from "./base-item-sheet.mjs"
|
||||
|
||||
export default class MournbladeMetierSheet extends MournbladeItemSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["metier"],
|
||||
position: {
|
||||
width: 620,
|
||||
},
|
||||
window: {
|
||||
contentClasses: ["metier-content"],
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: "systems/fvtt-mournblade/templates/item-metier-sheet.hbs",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "description",
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an array of form header tabs.
|
||||
* @returns {Record<string, Partial<ApplicationTab>>}
|
||||
*/
|
||||
#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
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import MournbladeItemSheet from "./base-item-sheet.mjs"
|
||||
|
||||
export default class MournbladeModifierSheet extends MournbladeItemSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["modifier"],
|
||||
position: {
|
||||
width: 620,
|
||||
},
|
||||
window: {
|
||||
contentClasses: ["modifier-content"],
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: "systems/fvtt-mournblade/templates/item-modifier-sheet.hbs",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "description",
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an array of form header tabs.
|
||||
* @returns {Record<string, Partial<ApplicationTab>>}
|
||||
*/
|
||||
#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
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import MournbladeItemSheet from "./base-item-sheet.mjs"
|
||||
|
||||
export default class MournbladeMonnaieSheet extends MournbladeItemSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["monnaie"],
|
||||
position: {
|
||||
width: 620,
|
||||
},
|
||||
window: {
|
||||
contentClasses: ["monnaie-content"],
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: "systems/fvtt-mournblade/templates/item-monnaie-sheet.hbs",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "description",
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an array of form header tabs.
|
||||
* @returns {Record<string, Partial<ApplicationTab>>}
|
||||
*/
|
||||
#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
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import MournbladeItemSheet from "./base-item-sheet.mjs"
|
||||
|
||||
export default class MournbladeOrigineSheet extends MournbladeItemSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["origine"],
|
||||
position: {
|
||||
width: 620,
|
||||
},
|
||||
window: {
|
||||
contentClasses: ["origine-content"],
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: "systems/fvtt-mournblade/templates/item-origine-sheet.hbs",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "description",
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an array of form header tabs.
|
||||
* @returns {Record<string, Partial<ApplicationTab>>}
|
||||
*/
|
||||
#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
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import MournbladeItemSheet from "./base-item-sheet.mjs"
|
||||
|
||||
export default class MournbladePacteSheet extends MournbladeItemSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["pacte"],
|
||||
position: {
|
||||
width: 620,
|
||||
},
|
||||
window: {
|
||||
contentClasses: ["pacte-content"],
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: "systems/fvtt-mournblade/templates/item-pacte-sheet.hbs",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "description",
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an array of form header tabs.
|
||||
* @returns {Record<string, Partial<ApplicationTab>>}
|
||||
*/
|
||||
#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
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import MournbladeActorSheet from "./base-actor-sheet.mjs"
|
||||
|
||||
export default class MournbladePersonnageSheet extends MournbladeActorSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
...super.DEFAULT_OPTIONS,
|
||||
classes: [...super.DEFAULT_OPTIONS.classes],
|
||||
window: {
|
||||
...super.DEFAULT_OPTIONS.window,
|
||||
title: "SHEETS.Actor.personnage",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
sheet: {
|
||||
template: "systems/fvtt-mournblade/templates/actor-sheet.hbs",
|
||||
scrollable: [".tab.principal", ".tab.competences", ".tab.dons", ".tab.equipement"],
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "principal",
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
const actor = this.document
|
||||
|
||||
// Add personnage-specific data
|
||||
context.skills = actor.getSkills()
|
||||
context.armes = foundry.utils.duplicate(actor.getWeapons())
|
||||
context.protections = foundry.utils.duplicate(actor.getArmors())
|
||||
context.dons = foundry.utils.duplicate(actor.getDons())
|
||||
context.pactes = foundry.utils.duplicate(actor.getPactes())
|
||||
context.alignement = actor.getAlignement()
|
||||
context.isChaotique = context.alignement === "chaotique"
|
||||
context.isLoyal = context.alignement === "loyal"
|
||||
context.aspect = actor.getAspect()
|
||||
context.marge = actor.getMarge()
|
||||
context.tendances = foundry.utils.duplicate(actor.getTendances())
|
||||
context.runes = foundry.utils.duplicate(actor.getRunes())
|
||||
context.traitsChaotiques = foundry.utils.duplicate(actor.getTraitsChaotiques())
|
||||
context.traitsEspeces = foundry.utils.duplicate(actor.getTraitsEspeces())
|
||||
context.origine = foundry.utils.duplicate(actor.getOrigine() || {})
|
||||
context.heritage = foundry.utils.duplicate(actor.getHeritage() || {})
|
||||
context.metier = foundry.utils.duplicate(actor.getMetier() || {})
|
||||
context.combat = actor.getCombatValues()
|
||||
context.equipements = foundry.utils.duplicate(actor.getEquipments())
|
||||
context.potions = foundry.utils.duplicate(actor.getPotions())
|
||||
context.modifiers = foundry.utils.duplicate(actor.getModifiers())
|
||||
context.monnaies = foundry.utils.duplicate(actor.getMonnaies())
|
||||
context.runeEffects = foundry.utils.duplicate(actor.getRuneEffects())
|
||||
context.protectionTotal = actor.getProtectionTotal()
|
||||
context.santeMalus = actor.getStatusMalus()
|
||||
context.ameMalus = actor.getAmeMalus()
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Application de feuille de PNJ pour Mournblade
|
||||
*/
|
||||
import MournbladeActorSheet from "./base-actor-sheet.mjs"
|
||||
|
||||
export default class MournbladePnjSheet extends MournbladeActorSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
...super.DEFAULT_OPTIONS,
|
||||
classes: [...super.DEFAULT_OPTIONS.classes],
|
||||
window: {
|
||||
...super.DEFAULT_OPTIONS.window,
|
||||
title: "SHEETS.Actor.pnj",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
sheet: {
|
||||
template: "systems/fvtt-mournblade/templates/actor-sheet.hbs",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "principal",
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
const actor = this.document
|
||||
|
||||
// Add pnj-specific data
|
||||
context.skills = actor.getSkills()
|
||||
context.armes = foundry.utils.duplicate(actor.getWeapons())
|
||||
context.protections = foundry.utils.duplicate(actor.getArmors())
|
||||
context.dons = foundry.utils.duplicate(actor.getDons())
|
||||
context.pactes = foundry.utils.duplicate(actor.getPactes())
|
||||
context.combat = actor.getCombatValues()
|
||||
context.equipements = foundry.utils.duplicate(actor.getEquipments())
|
||||
context.protectionTotal = actor.getProtectionTotal()
|
||||
context.santeMalus = actor.getStatusMalus()
|
||||
context.ameMalus = actor.getAmeMalus()
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import MournbladeItemSheet from "./base-item-sheet.mjs"
|
||||
|
||||
export default class MournbladePotionSheet extends MournbladeItemSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["potion"],
|
||||
position: {
|
||||
width: 640,
|
||||
},
|
||||
window: {
|
||||
contentClasses: ["potion-content"],
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: "systems/fvtt-mournblade/templates/item-potion-sheet.hbs",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "details",
|
||||
}
|
||||
|
||||
#getTabs() {
|
||||
const tabs = {
|
||||
details: { id: "details", group: "primary", label: "Détails" },
|
||||
effets: { id: "effets", group: "primary", label: "Effets" },
|
||||
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
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
context.config = game.system.mournblade.config
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import MournbladeItemSheet from "./base-item-sheet.mjs"
|
||||
|
||||
export default class MournbladeProtectionSheet extends MournbladeItemSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["protection"],
|
||||
position: {
|
||||
width: 620,
|
||||
},
|
||||
window: {
|
||||
contentClasses: ["protection-content"],
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: "systems/fvtt-mournblade/templates/item-protection-sheet.hbs",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "description",
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an array of form header tabs.
|
||||
* @returns {Record<string, Partial<ApplicationTab>>}
|
||||
*/
|
||||
#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
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import MournbladeItemSheet from "./base-item-sheet.mjs"
|
||||
|
||||
export default class MournbladeRuneSheet extends MournbladeItemSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["rune"],
|
||||
position: {
|
||||
width: 620,
|
||||
},
|
||||
window: {
|
||||
contentClasses: ["rune-content"],
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: "systems/fvtt-mournblade/templates/item-rune-sheet.hbs",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "description",
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an array of form header tabs.
|
||||
* @returns {Record<string, Partial<ApplicationTab>>}
|
||||
*/
|
||||
#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
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import MournbladeItemSheet from "./base-item-sheet.mjs"
|
||||
|
||||
export default class MournbladeRuneEffectSheet extends MournbladeItemSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["runeeffect"],
|
||||
position: {
|
||||
width: 620,
|
||||
},
|
||||
window: {
|
||||
contentClasses: ["runeeffect-content"],
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: "systems/fvtt-mournblade/templates/item-runeeffect-sheet.hbs",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "description",
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an array of form header tabs.
|
||||
* @returns {Record<string, Partial<ApplicationTab>>}
|
||||
*/
|
||||
#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
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import MournbladeItemSheet from "./base-item-sheet.mjs"
|
||||
|
||||
export default class MournbladeTendanceSheet extends MournbladeItemSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["tendance"],
|
||||
position: {
|
||||
width: 620,
|
||||
},
|
||||
window: {
|
||||
contentClasses: ["tendance-content"],
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: "systems/fvtt-mournblade/templates/item-tendance-sheet.hbs",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "description",
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an array of form header tabs.
|
||||
* @returns {Record<string, Partial<ApplicationTab>>}
|
||||
*/
|
||||
#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
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import MournbladeItemSheet from "./base-item-sheet.mjs"
|
||||
|
||||
export default class MournbladeTraitChaotiqueSheet extends MournbladeItemSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["traitchaotique"],
|
||||
position: {
|
||||
width: 620,
|
||||
},
|
||||
window: {
|
||||
contentClasses: ["traitchaotique-content"],
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: "systems/fvtt-mournblade/templates/item-traitchaotique-sheet.hbs",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "description",
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an array of form header tabs.
|
||||
* @returns {Record<string, Partial<ApplicationTab>>}
|
||||
*/
|
||||
#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
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import MournbladeItemSheet from "./base-item-sheet.mjs"
|
||||
|
||||
export default class MournbladeTraitEspeceSheet extends MournbladeItemSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["traitespece"],
|
||||
position: {
|
||||
width: 620,
|
||||
},
|
||||
window: {
|
||||
contentClasses: ["traitespece-content"],
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: "systems/fvtt-mournblade/templates/item-traitespece-sheet.hbs",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
tabGroups = {
|
||||
primary: "description",
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an array of form header tabs.
|
||||
* @returns {Record<string, Partial<ApplicationTab>>}
|
||||
*/
|
||||
#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
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Data model pour les armes
|
||||
*/
|
||||
export default class ArmeDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({ initial: "" }),
|
||||
typearme: new fields.StringField({ initial: "" }),
|
||||
isdefense: new fields.BooleanField({ initial: false }),
|
||||
bonusmaniementoff: new fields.NumberField({ initial: 0, integer: true }),
|
||||
bonusmaniementdef: new fields.NumberField({ initial: 0, integer: true }),
|
||||
ignorearmure: new fields.BooleanField({ initial: false }),
|
||||
nbressources: new fields.NumberField({ initial: 0, integer: true }),
|
||||
degats: new fields.StringField({ initial: "" }),
|
||||
nonletaux: new fields.BooleanField({ initial: false }),
|
||||
deuxmains: new fields.BooleanField({ initial: false }),
|
||||
courte: new fields.NumberField({ initial: 0, integer: true }),
|
||||
moyenne: new fields.NumberField({ initial: 0, integer: true }),
|
||||
longue: new fields.NumberField({ initial: 0, integer: true }),
|
||||
tr: new fields.NumberField({ initial: 0, integer: true }),
|
||||
rarete: new fields.NumberField({ initial: 0, integer: true }),
|
||||
prix: new fields.NumberField({ initial: 0, integer: true }),
|
||||
equipped: new fields.BooleanField({ initial: false }),
|
||||
enchantementLoi: new fields.SchemaField({
|
||||
actif: new fields.BooleanField({ initial: false }),
|
||||
bonus: new fields.NumberField({ initial: 0, integer: true }),
|
||||
antiChaos: new fields.BooleanField({ initial: false }),
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Data model de base pour les items Mournblade
|
||||
*/
|
||||
export default class BaseItemDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({ initial: "" })
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Data model pour les boucliers
|
||||
*/
|
||||
export default class BouclierDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({ initial: "" }),
|
||||
bonusdefense: new fields.NumberField({ initial: 0, integer: true }),
|
||||
degats: new fields.StringField({ initial: "" }),
|
||||
nonletaux: new fields.BooleanField({ initial: false }),
|
||||
rarete: new fields.NumberField({ initial: 0, integer: true }),
|
||||
prix: new fields.NumberField({ initial: 0, integer: true }),
|
||||
equipped: new fields.BooleanField({ initial: false }),
|
||||
enchantementLoi: new fields.SchemaField({
|
||||
actif: new fields.BooleanField({ initial: false }),
|
||||
bonus: new fields.NumberField({ initial: 0, integer: true }),
|
||||
antiChaos: new fields.BooleanField({ initial: false }),
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Data model pour les capacités
|
||||
*/
|
||||
export default class CapaciteDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
typeCapacite: new fields.StringField({ initial: "creature", blank: false }),
|
||||
description: new fields.HTMLField({ initial: "" })
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Data model pour les compétences
|
||||
*/
|
||||
export default class CompetenceDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({ initial: "" }),
|
||||
niveau: new fields.NumberField({ initial: 0, integer: true }),
|
||||
attribut1: new fields.StringField({ initial: "" }),
|
||||
attribut2: new fields.StringField({ initial: "" }),
|
||||
attribut3: new fields.StringField({ initial: "" }),
|
||||
doublebonus: new fields.BooleanField({ initial: false }),
|
||||
predilections: new fields.ArrayField(
|
||||
new fields.SchemaField({
|
||||
name: new fields.StringField({ initial: "" }),
|
||||
used: new fields.BooleanField({ initial: false })
|
||||
}),
|
||||
{ initial: [] }
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
static migrateData(source) {
|
||||
if (Array.isArray(source.predilections)) {
|
||||
source.predilections = source.predilections.map(pred =>
|
||||
typeof pred === "string" ? { name: pred, used: false } : pred
|
||||
);
|
||||
}
|
||||
return super.migrateData(source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Data model pour les créatures
|
||||
*/
|
||||
export default class CreatureDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
// Template biodata
|
||||
biodata: new fields.SchemaField({
|
||||
name: new fields.StringField({ initial: "" }),
|
||||
age: new fields.NumberField({ initial: 0, integer: true }),
|
||||
alignement: new fields.StringField({ initial: "" }),
|
||||
creatureType: new fields.StringField({ initial: "creature" }),
|
||||
elementType: new fields.StringField({ initial: "" }),
|
||||
demonType: new fields.StringField({ initial: "" }),
|
||||
demonPuissance: new fields.StringField({ initial: "" }),
|
||||
automatonType: new fields.StringField({ initial: "" }),
|
||||
automatonPuissance: new fields.StringField({ initial: "" }),
|
||||
automatonVoyageType: new fields.StringField({ initial: "" }),
|
||||
poids: new fields.StringField({ initial: "" }),
|
||||
taille: new fields.StringField({ initial: "" }),
|
||||
cheveux: new fields.StringField({ initial: "" }),
|
||||
sexe: new fields.StringField({ initial: "" }),
|
||||
yeux: new fields.StringField({ initial: "" }),
|
||||
description: new fields.HTMLField({ initial: "" }),
|
||||
amemultiplier: new fields.NumberField({ initial: 2, integer: true }),
|
||||
ignoreamemalus: new fields.BooleanField({ initial: false }),
|
||||
ignoresantemalus: new fields.BooleanField({ initial: false }),
|
||||
notes: new fields.HTMLField({ initial: "" }),
|
||||
gmnotes: new fields.HTMLField({ initial: "" })
|
||||
}),
|
||||
// Template core
|
||||
subactors: new fields.ArrayField(new fields.StringField(), { initial: [] }),
|
||||
attributs: new fields.SchemaField({
|
||||
adr: new fields.SchemaField({
|
||||
label: new fields.StringField({ initial: "Adresse" }),
|
||||
labelnorm: new fields.StringField({ initial: "adresse" }),
|
||||
abbrev: new fields.StringField({ initial: "adr" }),
|
||||
value: new fields.NumberField({ initial: 1, integer: true })
|
||||
}),
|
||||
pui: new fields.SchemaField({
|
||||
label: new fields.StringField({ initial: "Puissance" }),
|
||||
labelnorm: new fields.StringField({ initial: "puissance" }),
|
||||
abbrev: new fields.StringField({ initial: "pui" }),
|
||||
value: new fields.NumberField({ initial: 1, integer: true })
|
||||
}),
|
||||
cla: new fields.SchemaField({
|
||||
label: new fields.StringField({ initial: "Clairvoyance" }),
|
||||
labelnorm: new fields.StringField({ initial: "clairvoyance" }),
|
||||
abbrev: new fields.StringField({ initial: "cla" }),
|
||||
value: new fields.NumberField({ initial: 1, integer: true })
|
||||
}),
|
||||
pre: new fields.SchemaField({
|
||||
label: new fields.StringField({ initial: "Présence" }),
|
||||
labelnorm: new fields.StringField({ initial: "presence" }),
|
||||
abbrev: new fields.StringField({ initial: "pre" }),
|
||||
value: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
tre: new fields.SchemaField({
|
||||
label: new fields.StringField({ initial: "Trempe" }),
|
||||
labelnorm: new fields.StringField({ initial: "trempe" }),
|
||||
abbrev: new fields.StringField({ initial: "tre" }),
|
||||
value: new fields.NumberField({ initial: 0, integer: true })
|
||||
})
|
||||
}),
|
||||
bonneaventure: new fields.SchemaField({
|
||||
base: new fields.NumberField({ initial: 0, integer: true }),
|
||||
actuelle: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
experience: new fields.SchemaField({
|
||||
value: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
eclat: new fields.SchemaField({
|
||||
value: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
sante: new fields.SchemaField({
|
||||
base: new fields.NumberField({ initial: 0, integer: true }),
|
||||
bonus: new fields.NumberField({ initial: 0, integer: true }),
|
||||
nonletaux: new fields.NumberField({ initial: 0, integer: true }),
|
||||
letaux: new fields.NumberField({ initial: 0, integer: true }),
|
||||
malusmanuel: new fields.NumberField({ initial: 0, integer: true }),
|
||||
sequelles: new fields.StringField({ initial: "" })
|
||||
}),
|
||||
ame: new fields.SchemaField({
|
||||
fullmax: new fields.NumberField({ initial: 0, integer: true }),
|
||||
currentmax: new fields.NumberField({ initial: 0, integer: true }),
|
||||
value: new fields.NumberField({ initial: 0, integer: true }),
|
||||
traumatismes: new fields.StringField({ initial: "" })
|
||||
}),
|
||||
combat: new fields.SchemaField({
|
||||
initbonus: new fields.NumberField({ initial: 0, integer: true }),
|
||||
vitessebonus: new fields.NumberField({ initial: 0, integer: true }),
|
||||
bonusdegats: new fields.NumberField({ initial: 0, integer: true }),
|
||||
defensebonus: new fields.NumberField({ initial: 0, integer: true }),
|
||||
monte: new fields.BooleanField({ initial: false })
|
||||
}),
|
||||
balance: new fields.SchemaField({
|
||||
loi: new fields.NumberField({ initial: 0, integer: true }),
|
||||
chaos: new fields.NumberField({ initial: 0, integer: true }),
|
||||
aspect: new fields.NumberField({ initial: 0, integer: true }),
|
||||
marge: new fields.NumberField({ initial: 0, integer: true }),
|
||||
pointschaos: new fields.NumberField({ initial: 0, integer: true }),
|
||||
pointsloi: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
ressources: new fields.SchemaField({
|
||||
value: new fields.NumberField({ initial: 0, integer: true })
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Data model pour les dons
|
||||
*/
|
||||
export default class DonDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({ initial: "" }),
|
||||
allegeance: new fields.StringField({ initial: "" }),
|
||||
prerequis: new fields.StringField({ initial: "" }),
|
||||
sacrifice: new fields.HTMLField({ initial: "" })
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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: "" }),
|
||||
rarete: new fields.NumberField({ initial: 0, integer: true }),
|
||||
prix: new fields.NumberField({ initial: 0, integer: true }),
|
||||
enchantementLoi: new fields.SchemaField({
|
||||
actif: new fields.BooleanField({ initial: false }),
|
||||
bonus: new fields.NumberField({ initial: 0, integer: true }),
|
||||
antiChaos: new fields.BooleanField({ initial: false }),
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Data model pour les héritages
|
||||
*/
|
||||
export default class HeritageDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({ initial: "" })
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Index des DataModels pour Mournblade
|
||||
* Ce fichier centralise tous les exports des modèles de données
|
||||
*/
|
||||
|
||||
// Modèles d'items
|
||||
export { default as ArmeDataModel } from './arme.mjs';
|
||||
export { default as BouclierDataModel } from './bouclier.mjs';
|
||||
export { default as CapaciteDataModel } from './capacite.mjs';
|
||||
export { default as CompetenceDataModel } from './competence.mjs';
|
||||
export { default as DonDataModel } from './don.mjs';
|
||||
export { default as EquipementDataModel } from './equipement.mjs';
|
||||
export { default as HeritageDataModel } from './heritage.mjs';
|
||||
export { default as MetierDataModel } from './metier.mjs';
|
||||
export { default as ModifierDataModel } from './modifier.mjs';
|
||||
export { default as MonnaieDataModel } from './monnaie.mjs';
|
||||
export { default as OrigineDataModel } from './origine.mjs';
|
||||
export { default as PacteDataModel } from './pacte.mjs';
|
||||
export { default as ProtectionDataModel } from './protection.mjs';
|
||||
export { default as RuneDataModel } from './rune.mjs';
|
||||
export { default as RuneEffectDataModel } from './runeeffect.mjs';
|
||||
export { default as PotionDataModel } from './potion.mjs';
|
||||
export { default as TendanceDataModel } from './tendance.mjs';
|
||||
export { default as TraitChaotiqueDataModel } from './traitchaotique.mjs';
|
||||
export { default as TraitEspeceDataModel } from './traitespece.mjs';
|
||||
|
||||
// Modèles d'acteurs
|
||||
export { default as PersonnageDataModel } from './personnage.mjs';
|
||||
export { default as CreatureDataModel } from './creature.mjs';
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Data model pour les métiers
|
||||
*/
|
||||
export default class MetierDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({ initial: "" })
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Data model pour les modificateurs
|
||||
*/
|
||||
export default class ModifierDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({ initial: "" }),
|
||||
modifiertype: new fields.StringField({ initial: "roll" }),
|
||||
value: new fields.NumberField({ initial: 0, integer: true }),
|
||||
attribut: new fields.StringField({ initial: "aucun" }),
|
||||
competence: new fields.StringField({ initial: "aucun" }),
|
||||
permanent: new fields.BooleanField({ initial: true }),
|
||||
once: new fields.BooleanField({ initial: false }),
|
||||
duree: new fields.StringField({ initial: "" })
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Data model pour les monnaies
|
||||
*/
|
||||
export default class MonnaieDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({ initial: "" }),
|
||||
quantite: new fields.NumberField({ initial: 0, integer: true }),
|
||||
unite: new fields.StringField({ initial: "" })
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Data model pour les origines
|
||||
*/
|
||||
export default class OrigineDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({ initial: "" })
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Data model pour les pactes
|
||||
*/
|
||||
export default class PacteDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({ initial: "" }),
|
||||
allegeance: new fields.StringField({ initial: "" })
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Data model pour les personnages
|
||||
*/
|
||||
export default class PersonnageDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
// Template biodata
|
||||
biodata: new fields.SchemaField({
|
||||
name: new fields.StringField({ initial: "" }),
|
||||
age: new fields.NumberField({ initial: 0, integer: true }),
|
||||
alignement: new fields.StringField({ initial: "" }),
|
||||
poids: new fields.StringField({ initial: "" }),
|
||||
taille: new fields.StringField({ initial: "" }),
|
||||
cheveux: new fields.StringField({ initial: "" }),
|
||||
sexe: new fields.StringField({ initial: "" }),
|
||||
yeux: new fields.StringField({ initial: "" }),
|
||||
description: new fields.HTMLField({ initial: "" }),
|
||||
amemultiplier: new fields.NumberField({ initial: 2, integer: true }),
|
||||
ignoreamemalus: new fields.BooleanField({ initial: false }),
|
||||
ignoresantemalus: new fields.BooleanField({ initial: false }),
|
||||
notes: new fields.HTMLField({ initial: "" }),
|
||||
gmnotes: new fields.HTMLField({ initial: "" })
|
||||
}),
|
||||
// Template core
|
||||
subactors: new fields.ArrayField(new fields.StringField(), { initial: [] }),
|
||||
attributs: new fields.SchemaField({
|
||||
adr: new fields.SchemaField({
|
||||
label: new fields.StringField({ initial: "Adresse" }),
|
||||
labelnorm: new fields.StringField({ initial: "adresse" }),
|
||||
abbrev: new fields.StringField({ initial: "adr" }),
|
||||
value: new fields.NumberField({ initial: 1, integer: true })
|
||||
}),
|
||||
pui: new fields.SchemaField({
|
||||
label: new fields.StringField({ initial: "Puissance" }),
|
||||
labelnorm: new fields.StringField({ initial: "puissance" }),
|
||||
abbrev: new fields.StringField({ initial: "pui" }),
|
||||
value: new fields.NumberField({ initial: 1, integer: true })
|
||||
}),
|
||||
cla: new fields.SchemaField({
|
||||
label: new fields.StringField({ initial: "Clairvoyance" }),
|
||||
labelnorm: new fields.StringField({ initial: "clairvoyance" }),
|
||||
abbrev: new fields.StringField({ initial: "cla" }),
|
||||
value: new fields.NumberField({ initial: 1, integer: true })
|
||||
}),
|
||||
pre: new fields.SchemaField({
|
||||
label: new fields.StringField({ initial: "Présence" }),
|
||||
labelnorm: new fields.StringField({ initial: "presence" }),
|
||||
abbrev: new fields.StringField({ initial: "pre" }),
|
||||
value: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
tre: new fields.SchemaField({
|
||||
label: new fields.StringField({ initial: "Trempe" }),
|
||||
labelnorm: new fields.StringField({ initial: "trempe" }),
|
||||
abbrev: new fields.StringField({ initial: "tre" }),
|
||||
value: new fields.NumberField({ initial: 0, integer: true })
|
||||
})
|
||||
}),
|
||||
bonneaventure: new fields.SchemaField({
|
||||
base: new fields.NumberField({ initial: 0, integer: true }),
|
||||
actuelle: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
experience: new fields.SchemaField({
|
||||
value: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
eclat: new fields.SchemaField({
|
||||
value: new fields.NumberField({ initial: 0, integer: true })
|
||||
}),
|
||||
sante: new fields.SchemaField({
|
||||
base: new fields.NumberField({ initial: 0, integer: true }),
|
||||
bonus: new fields.NumberField({ initial: 0, integer: true }),
|
||||
nonletaux: new fields.NumberField({ initial: 0, integer: true }),
|
||||
letaux: new fields.NumberField({ initial: 0, integer: true }),
|
||||
malusmanuel: new fields.NumberField({ initial: 0, integer: true }),
|
||||
sequelles: new fields.StringField({ initial: "" })
|
||||
}),
|
||||
ame: new fields.SchemaField({
|
||||
fullmax: new fields.NumberField({ initial: 0, integer: true }),
|
||||
currentmax: new fields.NumberField({ initial: 0, integer: true }),
|
||||
value: new fields.NumberField({ initial: 0, integer: true }),
|
||||
traumatismes: new fields.StringField({ initial: "" })
|
||||
}),
|
||||
invocationsElementaires: new fields.ArrayField(new fields.ObjectField(), { initial: [] }),
|
||||
invocationsDemons: new fields.ArrayField(new fields.ObjectField(), { initial: [] }),
|
||||
combat: new fields.SchemaField({
|
||||
initbonus: new fields.NumberField({ initial: 0, integer: true }),
|
||||
vitessebonus: new fields.NumberField({ initial: 0, integer: true }),
|
||||
bonusdegats: new fields.NumberField({ initial: 0, integer: true }),
|
||||
defensebonus: new fields.NumberField({ initial: 0, integer: true }),
|
||||
monte: new fields.BooleanField({ initial: false })
|
||||
}),
|
||||
balance: new fields.SchemaField({
|
||||
loi: new fields.NumberField({ initial: 0, integer: true }),
|
||||
chaos: new fields.NumberField({ initial: 0, integer: true }),
|
||||
aspect: new fields.NumberField({ initial: 0, integer: true }),
|
||||
marge: new fields.NumberField({ initial: 0, integer: true }),
|
||||
pointschaos: new fields.NumberField({ initial: 0, integer: true }),
|
||||
pointsloi: new fields.NumberField({ initial: 0, integer: true })
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Data model pour les potions à base de runes
|
||||
*/
|
||||
export default class PotionDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({ initial: "" }),
|
||||
effetCuratif: new fields.HTMLField({ initial: "" }),
|
||||
effetLetal: new fields.HTMLField({ initial: "" }),
|
||||
effetSecondaire: new fields.HTMLField({ initial: "" }),
|
||||
rune: new fields.StringField({ initial: "" }),
|
||||
runeImg: new fields.StringField({ initial: "" }),
|
||||
runeSeuil: new fields.NumberField({ initial: 0, integer: true }),
|
||||
pointsAme: new fields.NumberField({ initial: 1, integer: true, min: 1 }),
|
||||
forme: new fields.StringField({ initial: "liquide" }),
|
||||
statut: new fields.StringField({ initial: "inconnue" }),
|
||||
virulence: new fields.NumberField({ initial: 0, integer: true }),
|
||||
duree: new fields.StringField({ initial: "" }),
|
||||
conservation: new fields.StringField({ initial: "" }),
|
||||
tempsPreparation: new fields.StringField({ initial: "" }),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Data model pour les protections
|
||||
*/
|
||||
export default class ProtectionDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({ initial: "" }),
|
||||
typeprotection: new fields.StringField({ initial: "" }),
|
||||
protection: new fields.NumberField({ initial: 0, integer: true }),
|
||||
degats: new fields.StringField({ initial: "" }),
|
||||
rarete: new fields.NumberField({ initial: 0, integer: true }),
|
||||
prix: new fields.NumberField({ initial: 0, integer: true }),
|
||||
equipped: new fields.BooleanField({ initial: false }),
|
||||
enchantementLoi: new fields.SchemaField({
|
||||
actif: new fields.BooleanField({ initial: false }),
|
||||
bonus: new fields.NumberField({ initial: 0, integer: true }),
|
||||
antiChaos: new fields.BooleanField({ initial: false }),
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Data model pour les runes
|
||||
*/
|
||||
export default class RuneDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({ initial: "" }),
|
||||
formule: new fields.StringField({ initial: "" }),
|
||||
seuil: new fields.NumberField({ initial: 0, integer: true }),
|
||||
prononcee: new fields.StringField({ initial: "" }),
|
||||
tracee: new fields.StringField({ initial: "" })
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Data model pour les effets de runes
|
||||
*/
|
||||
export default class RuneEffectDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({ initial: "" }),
|
||||
rune: new fields.StringField({ initial: "" }),
|
||||
mode: new fields.StringField({ initial: "" }),
|
||||
duree: new fields.StringField({ initial: "" }),
|
||||
pointame: new fields.NumberField({ initial: 0, integer: true })
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Data model pour les tendances
|
||||
*/
|
||||
export default class TendanceDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({ initial: "" }),
|
||||
allegeance: new fields.StringField({ initial: "" })
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Data model pour les traits chaotiques
|
||||
*/
|
||||
export default class TraitChaotiqueDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({ initial: "" })
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Data model pour les traits d'espèce
|
||||
*/
|
||||
export default class TraitEspeceDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields;
|
||||
return {
|
||||
description: new fields.HTMLField({ initial: "" })
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
/* -------------------------------------------- */
|
||||
import { MournbladeUtility } from "./mournblade-utility.js";
|
||||
import { MournbladeRollDialog } from "./mournblade-roll-dialog.js";
|
||||
import { MournbladeRollDialog } from "./applications/mournblade-roll-dialog.mjs";
|
||||
import MournbladeInvocationDialog from "./applications/mournblade-invocation-dialog.mjs";
|
||||
import MournbladeInvocationDemonDialog from "./applications/mournblade-invocation-demon-dialog.mjs";
|
||||
import MournbladeInvocationEspritDialog from "./applications/mournblade-invocation-esprit-dialog.mjs";
|
||||
import MournbladeEnchantementDialog from "./applications/mournblade-enchantement-dialog.mjs";
|
||||
|
||||
/* -------------------------------------------- */
|
||||
const __degatsBonus = [-2, -2, -1, -1, 0, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 8, 8, 9, 9, 10, 10]
|
||||
@@ -12,6 +16,34 @@ const __vitesseBonus = [-2, -2, -1, -1, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6
|
||||
* @extends {Actor}
|
||||
*/
|
||||
export class MournbladeActor extends Actor {
|
||||
prepareData() {
|
||||
super.prepareData();
|
||||
|
||||
// Calculate derived attributes
|
||||
const data = this.system;
|
||||
|
||||
if (this.type == 'personnage') {
|
||||
// Compute base health and max soul from attributes (derived, no DB write needed)
|
||||
data.sante.base = data.sante.bonus + (data.attributs.pui.value + data.attributs.tre.value) * 2 + 5;
|
||||
data.ame.fullmax = (data.attributs.cla.value + data.attributs.tre.value) * data.biodata.amemultiplier + 5;
|
||||
}
|
||||
|
||||
// Calculate total health
|
||||
data.sante.total = data.sante.base + data.sante.bonus;
|
||||
|
||||
// Calculate current health
|
||||
data.sante.current = data.sante.total - data.sante.nonletaux - data.sante.letaux;
|
||||
|
||||
// Calculate total ame
|
||||
data.ame.total = data.ame.fullmax;
|
||||
|
||||
// Calculate balance
|
||||
data.balance.total = data.balance.loi + data.balance.chaos;
|
||||
}
|
||||
|
||||
prepareDerivedData() {
|
||||
super.prepareDerivedData();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/**
|
||||
@@ -62,25 +94,43 @@ export class MournbladeActor extends Actor {
|
||||
prepareArme(arme) {
|
||||
arme = foundry.utils.duplicate(arme)
|
||||
let combat = this.getCombatValues()
|
||||
const enchBonus = (arme.system.enchantementLoi?.actif && arme.system.enchantementLoi?.bonus > 0)
|
||||
? arme.system.enchantementLoi.bonus : 0
|
||||
if (arme.system.typearme == "contact" || arme.system.typearme == "contactjet") {
|
||||
arme.system.isMelee = true
|
||||
arme.system.competence = foundry.utils.duplicate(this.items.find(item => item.type == "competence" && item.name.toLowerCase() == "mêlée"))
|
||||
let competence = this.items.find(item => item.type == "competence" && item.name.toLowerCase() == "mêlée")
|
||||
if (competence) {
|
||||
arme.system.competence = foundry.utils.duplicate(competence)
|
||||
arme.system.attrKey = "pui"
|
||||
arme.system.totalDegats = arme.system.degats + "+" + combat.bonusDegatsTotal
|
||||
arme.system.totalOffensif = this.system.attributs.pui.value + arme.system.competence.system.niveau + arme.system.bonusmaniementoff + combat.attaqueModifier
|
||||
arme.system.totalOffensif = this.system.attributs.pui.value + arme.system.competence.system.niveau + arme.system.bonusmaniementoff + combat.attaqueModifier + enchBonus
|
||||
if (arme.system.isdefense) {
|
||||
arme.system.totalDefensif = combat.defenseTotal + arme.system.competence.system.niveau + arme.system.bonusmaniementdef
|
||||
}
|
||||
} else {
|
||||
arme.system.competence = null
|
||||
arme.system.totalOffensif = 0
|
||||
arme.system.totalDegats = arme.system.degats
|
||||
arme.system.totalDefensif = 0
|
||||
}
|
||||
}
|
||||
if (arme.system.typearme == "jet" || arme.system.typearme == "tir") {
|
||||
arme.system.isDistance = true
|
||||
arme.system.competence = foundry.utils.duplicate(this.items.find(item => item.type == "competence" && item.name.toLowerCase() == "armes à distance"))
|
||||
let competence = this.items.find(item => item.type == "competence" && item.name.toLowerCase() == "armes à distance")
|
||||
if (competence) {
|
||||
arme.system.competence = foundry.utils.duplicate(competence)
|
||||
arme.system.attrKey = "adr"
|
||||
arme.system.totalOffensif = this.system.attributs.adr.value + arme.system.competence.system.niveau + arme.system.bonusmaniementoff + combat.attaqueModifier
|
||||
arme.system.totalOffensif = this.system.attributs.adr.value + arme.system.competence.system.niveau + arme.system.bonusmaniementoff + combat.attaqueModifier + enchBonus
|
||||
arme.system.totalDegats = arme.system.degats
|
||||
if (arme.system.isdefense) {
|
||||
arme.system.totalDefensif = combat.defenseTotal + arme.system.competence.system.niveau + arme.system.bonusmaniementdef
|
||||
}
|
||||
} else {
|
||||
arme.system.competence = null
|
||||
arme.system.totalOffensif = 0
|
||||
arme.system.totalDegats = arme.system.degats
|
||||
arme.system.totalDefensif = 0
|
||||
}
|
||||
}
|
||||
return arme
|
||||
}
|
||||
@@ -88,13 +138,23 @@ export class MournbladeActor extends Actor {
|
||||
prepareBouclier(bouclier) {
|
||||
bouclier = foundry.utils.duplicate(bouclier)
|
||||
let combat = this.getCombatValues()
|
||||
bouclier.system.competence = foundry.utils.duplicate(this.items.find(item => item.type == "competence" && item.name.toLowerCase() == "mêlée"))
|
||||
const enchBonus = (bouclier.system.enchantementLoi?.actif && bouclier.system.enchantementLoi?.bonus > 0)
|
||||
? bouclier.system.enchantementLoi.bonus : 0
|
||||
let competence = this.items.find(item => item.type == "competence" && item.name.toLowerCase() == "mêlée")
|
||||
if (competence) {
|
||||
bouclier.system.competence = foundry.utils.duplicate(competence)
|
||||
bouclier.system.attrKey = "pui"
|
||||
bouclier.system.totalDegats = bouclier.system.degats + "+" + combat.bonusDegatsTotal
|
||||
bouclier.system.totalOffensif = this.system.attributs.pui.value + bouclier.system.competence.system.niveau
|
||||
bouclier.system.totalOffensif = this.system.attributs.pui.value + bouclier.system.competence.system.niveau + enchBonus
|
||||
bouclier.system.isdefense = true
|
||||
bouclier.system.bonusmaniementoff = 0
|
||||
bouclier.system.totalDefensif = combat.defenseTotal + bouclier.system.competence.system.niveau + bouclier.system.bonusdefense
|
||||
} else {
|
||||
bouclier.system.competence = null
|
||||
bouclier.system.totalOffensif = 0
|
||||
bouclier.system.totalDegats = bouclier.system.degats
|
||||
bouclier.system.totalDefensif = 0
|
||||
}
|
||||
return bouclier
|
||||
}
|
||||
|
||||
@@ -160,9 +220,15 @@ export class MournbladeActor extends Actor {
|
||||
getMonnaies() {
|
||||
return this.getItemSorted(["monnaie"])
|
||||
}
|
||||
getPotions() {
|
||||
return this.getItemSorted(["potion"])
|
||||
}
|
||||
getArmors() {
|
||||
return this.getItemSorted(["protection"])
|
||||
}
|
||||
getCapacites() {
|
||||
return this.getItemSorted(["capacite"])
|
||||
}
|
||||
getRuneEffects() {
|
||||
return this.getItemSorted(["runeeffect"])
|
||||
}
|
||||
@@ -228,7 +294,7 @@ export class MournbladeActor extends Actor {
|
||||
|
||||
/* -------------------------------------------- */
|
||||
getVitesseBase() {
|
||||
return 5 + __vitesseBonus[this.system.attributs.adr.value]
|
||||
return 5 + __vitesseBonus[Math.min(21, Math.max(0, this.system.attributs.adr.value))]
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
@@ -253,23 +319,6 @@ export class MournbladeActor extends Actor {
|
||||
return combat
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
prepareDerivedData() {
|
||||
|
||||
if (this.type == 'personnage') {
|
||||
let newSante = this.system.sante.bonus + (this.system.attributs.pui.value + this.system.attributs.tre.value) * 2 + 5
|
||||
if (this.system.sante.base != newSante) {
|
||||
this.update({ 'system.sante.base': newSante })
|
||||
}
|
||||
let newAme = (this.system.attributs.cla.value + this.system.attributs.tre.value) * this.system.biodata.amemultiplier + 5
|
||||
if (this.system.ame.fullmax != newAme) {
|
||||
this.update({ 'system.ame.fullmax': newAme })
|
||||
}
|
||||
}
|
||||
|
||||
super.prepareDerivedData()
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
_preUpdate(changed, options, user) {
|
||||
|
||||
@@ -347,7 +396,7 @@ export class MournbladeActor extends Actor {
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
incDecSante(type, value, applyArmure = true) {
|
||||
async incDecSante(type, value, applyArmure = true) {
|
||||
value = Number(value)
|
||||
if (value && applyArmure) {
|
||||
let protection = this.getProtectionTotal()
|
||||
@@ -368,19 +417,19 @@ export class MournbladeActor extends Actor {
|
||||
if (value && type == "nonletaux") {
|
||||
newSante["letaux"] += value
|
||||
}
|
||||
this.update({ 'system.sante': newSante })
|
||||
await this.update({ 'system.sante': newSante })
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
incDecAme(value) {
|
||||
async incDecAme(value) {
|
||||
value = Number(value)
|
||||
if (value) {
|
||||
let newAme = foundry.utils.duplicate(this.system.ame)
|
||||
newAme.value += Number(value)
|
||||
newAme.value = Math.max(0, newAme.value)
|
||||
newAme.value = Math.min(newAme.value, newAme.currentmax)
|
||||
this.update({ 'system.ame': newAme })
|
||||
await this.update({ 'system.ame': newAme })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,10 +439,10 @@ export class MournbladeActor extends Actor {
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
changeBonneAventure(value) {
|
||||
async changeBonneAventure(value) {
|
||||
let newBA = this.system.bonneaventure.actuelle
|
||||
newBA += value
|
||||
this.update({ 'system.bonneaventure.actuelle': newBA })
|
||||
await this.update({ 'system.bonneaventure.actuelle': newBA })
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
@@ -402,10 +451,10 @@ export class MournbladeActor extends Actor {
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
changeEclat(value) {
|
||||
async changeEclat(value) {
|
||||
let newE = this.system.eclat.value
|
||||
newE += value
|
||||
this.update({ 'system.eclat.value': newE })
|
||||
await this.update({ 'system.eclat.value': newE })
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
@@ -420,7 +469,7 @@ export class MournbladeActor extends Actor {
|
||||
} else {
|
||||
ame.currentmax -= value
|
||||
}
|
||||
this.update({ 'system.ame': ame })
|
||||
return this.update({ 'system.ame': ame })
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
@@ -436,7 +485,7 @@ export class MournbladeActor extends Actor {
|
||||
|
||||
/* -------------------------------------------- */
|
||||
getAttribute(attrKey) {
|
||||
return this.system.attributes[attrKey]
|
||||
return this.system.attributs[attrKey]
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
@@ -444,7 +493,7 @@ export class MournbladeActor extends Actor {
|
||||
if (this.type == "creature") {
|
||||
return 0
|
||||
}
|
||||
return __degatsBonus[this.system.attributs.pui.value]
|
||||
return __degatsBonus[Math.min(21, Math.max(0, this.system.attributs.pui.value))]
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
@@ -535,8 +584,8 @@ export class MournbladeActor extends Actor {
|
||||
this.update({ 'system.ressources': ressources })
|
||||
ChatMessage.create({
|
||||
content: "L'utilisation de la capacité/arme a dépensé " + arme.system.nbressources + " ressources.",
|
||||
whisper: game.user._id,
|
||||
user: game.user._id
|
||||
whisper: game.user.id,
|
||||
user: game.user.id
|
||||
});
|
||||
} else {
|
||||
ui.notifications.warn("Points de ressources insuffisants.")
|
||||
@@ -589,16 +638,14 @@ export class MournbladeActor extends Actor {
|
||||
/* -------------------------------------------- */
|
||||
async rollAttribut(attrKey) {
|
||||
let rollData = this.getCommonRollData(attrKey)
|
||||
let rollDialog = await MournbladeRollDialog.create(this, rollData)
|
||||
rollDialog.render(true)
|
||||
await MournbladeRollDialog.create(this, rollData)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
async rollCompetence(attrKey, compId) {
|
||||
let rollData = this.getCommonRollData(attrKey, compId)
|
||||
console.log("RollDatra", rollData)
|
||||
let rollDialog = await MournbladeRollDialog.create(this, rollData)
|
||||
rollDialog.render(true)
|
||||
await MournbladeRollDialog.create(this, rollData)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
@@ -614,8 +661,176 @@ export class MournbladeActor extends Actor {
|
||||
rollData.runemode = "prononcer"
|
||||
rollData.runeame = 1
|
||||
console.log("runeData", rollData)
|
||||
let rollDialog = await MournbladeRollDialog.create(this, rollData)
|
||||
rollDialog.render(true)
|
||||
await MournbladeRollDialog.create(this, rollData)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
async rollSortilege() {
|
||||
const runes = this.getRunes()
|
||||
if (runes.length < 2) {
|
||||
ui.notifications.warn("Il faut au moins deux Runes pour lancer un Sortilège.")
|
||||
return
|
||||
}
|
||||
const comp = this.items.find(c => c.type == "competence" && c.name.toLowerCase() == "savoir : runes")
|
||||
if (!comp) {
|
||||
ui.notifications.warn("La compétence Savoir : Runes n'a pas été trouvée, abandon.")
|
||||
return
|
||||
}
|
||||
let rollData = this.getCommonRollData("cla", undefined, "Savoir : Runes")
|
||||
rollData.isSortilege = true
|
||||
rollData.difficulte = 0
|
||||
rollData.runemode = "prononcer"
|
||||
rollData.runeame = 0
|
||||
await MournbladeRollDialog.createSortilege(this, rollData)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
async preparePotion() {
|
||||
const runes = this.getRunes()
|
||||
if (runes.length === 0) {
|
||||
ui.notifications.warn("Aucune Rune disponible pour préparer une potion.")
|
||||
return
|
||||
}
|
||||
const comp = this.items.find(c => c.type == "competence" && c.name.toLowerCase() == "savoir : runes")
|
||||
if (!comp) {
|
||||
ui.notifications.warn("La compétence Savoir : Runes n'a pas été trouvée, abandon.")
|
||||
return
|
||||
}
|
||||
let rollData = this.getCommonRollData("cla", comp._id)
|
||||
rollData.isPotion = true
|
||||
rollData.mainDice = "1d10"
|
||||
|
||||
// Optionally cap by Savoir:Haut-Parler and Savoir:Alchimie
|
||||
const hautParlerComp = this.items.find(c => c.type == "competence" && c.name.toLowerCase() == "savoir : haut-parler")
|
||||
const alchimieComp = this.items.find(c => c.type == "competence" && c.name.toLowerCase() == "savoir : alchimie")
|
||||
if (hautParlerComp) rollData.limitHautParlerValue = hautParlerComp.system.niveau
|
||||
if (alchimieComp) rollData.limitAlchimieValue = alchimieComp.system.niveau
|
||||
|
||||
await MournbladeRollDialog.createPotion(this, rollData)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
async invoquerElementaire() {
|
||||
const persuasionComp = this.items.find(c => c.type == "competence" && c.name.toLowerCase() == "persuasion")
|
||||
if (!persuasionComp) {
|
||||
ui.notifications.warn("La compétence Persuasion est requise pour invoquer un Élémentaire.")
|
||||
return
|
||||
}
|
||||
let rollData = this.getCommonRollData("pre", persuasionComp._id)
|
||||
rollData.isInvocationElementaire = true
|
||||
rollData.mainDice = "1d10"
|
||||
|
||||
const hautParlerComp = this.items.find(c => c.type == "competence" && c.name.toLowerCase() == "savoir : haut-parler")
|
||||
const seigneursElemComp = this.items.find(c => c.type == "competence" && c.name.toLowerCase() == "savoir : seigneurs élémentaires")
|
||||
if (hautParlerComp) rollData.hautParlerNiveau = hautParlerComp.system.niveau
|
||||
if (seigneursElemComp) rollData.seigneursElemNiveau = seigneursElemComp.system.niveau
|
||||
|
||||
await MournbladeInvocationDialog.create(this, rollData)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
async bannirElementaire(invocIndex) {
|
||||
const invocations = foundry.utils.duplicate(this.system.invocationsElementaires || [])
|
||||
const invoc = invocations[invocIndex]
|
||||
if (!invoc) return
|
||||
|
||||
// Free the blocked soul
|
||||
await this.subPointsAme("prononcer", -invoc.soulCost)
|
||||
|
||||
// Delete the created actor if it still exists
|
||||
if (invoc.actorId) {
|
||||
const createdActor = game.actors.get(invoc.actorId)
|
||||
if (createdActor) await createdActor.delete()
|
||||
}
|
||||
|
||||
// Remove from the list
|
||||
invocations.splice(invocIndex, 1)
|
||||
await this.update({ "system.invocationsElementaires": invocations })
|
||||
ui.notifications.info(`L'Élémentaire ${invoc.actorName} a été banni. ${invoc.soulCost} points d'Âme libérés.`)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
async invoquerDemon() {
|
||||
const normalize = str => str.toLowerCase()
|
||||
.replace(/œ/g, "oe").replace(/æ/g, "ae")
|
||||
.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, " ").trim()
|
||||
const hasKeywords = (item, ...words) => {
|
||||
const n = normalize(item.name)
|
||||
return words.every(w => n.includes(normalize(w)))
|
||||
}
|
||||
const coercitionComp = this.items.find(c => c.type === "competence" && hasKeywords(c, "coercition"))
|
||||
if (!coercitionComp) {
|
||||
ui.notifications.warn("La compétence Coercition est requise pour invoquer un Démon.")
|
||||
return
|
||||
}
|
||||
const isChaotique = this.system.balance.chaos > this.system.balance.loi
|
||||
const hasOeilSorcier = this.items.some(c => (c.type === "capacite" || c.type === "don") && hasKeywords(c, "oeil", "sorcier"))
|
||||
const hasRuneChaos = this.items.some(i => i.type === "rune" && hasKeywords(i, "chaos"))
|
||||
if (!isChaotique || !hasOeilSorcier || !hasRuneChaos) {
|
||||
const missing = []
|
||||
if (!isChaotique) missing.push("alignement chaotique (Chaos > Loi)")
|
||||
if (!hasOeilSorcier) missing.push("Capacité/Don Œil du Sorcier")
|
||||
if (!hasRuneChaos) missing.push("Rune du Chaos")
|
||||
ui.notifications.warn(`Prérequis manquants pour l'invocation démoniaque : ${missing.join(", ")}.`)
|
||||
}
|
||||
|
||||
let rollData = this.getCommonRollData("tre", coercitionComp._id)
|
||||
rollData.isInvocationDemon = true
|
||||
rollData.mainDice = "1d10"
|
||||
|
||||
await MournbladeInvocationDemonDialog.create(this, rollData)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
async libererDemon(invocIndex) {
|
||||
const invocations = foundry.utils.duplicate(this.system.invocationsDemons || [])
|
||||
const invoc = invocations[invocIndex]
|
||||
if (!invoc) return
|
||||
|
||||
invocations.splice(invocIndex, 1)
|
||||
await this.update({ "system.invocationsDemons": invocations })
|
||||
ui.notifications.info(`Le Démon ${invoc.demonName ?? "invoqué"} a été libéré.`)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
async invoquerEspritLoi() {
|
||||
const normalize = str => str.toLowerCase()
|
||||
.replace(/œ/g, "oe").replace(/æ/g, "ae")
|
||||
.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, " ").trim()
|
||||
const hasKeywords = (item, ...words) => {
|
||||
const n = normalize(item.name)
|
||||
return words.every(w => n.includes(normalize(w)))
|
||||
}
|
||||
|
||||
const persuasionComp = this.items.find(c => c.type === "competence" && hasKeywords(c, "persuasion"))
|
||||
const isLoyal = this.system.balance.loi > this.system.balance.chaos
|
||||
const hasRuneLoi = this.items.some(i => i.type === "rune" && hasKeywords(i, "loi"))
|
||||
|
||||
if (!isLoyal || !hasRuneLoi) {
|
||||
const missing = []
|
||||
if (!isLoyal) missing.push("alignement loyal (Loi > Chaos)")
|
||||
if (!hasRuneLoi) missing.push("Rune de la Loi")
|
||||
ui.notifications.warn(`Prérequis manquants : ${missing.join(", ")}.`)
|
||||
}
|
||||
|
||||
const rollData = this.getCommonRollData("tre", persuasionComp?._id ?? null)
|
||||
rollData.isInvocationEsprit = true
|
||||
rollData.mainDice = "1d10"
|
||||
|
||||
await MournbladeInvocationEspritDialog.create(this, rollData)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
async enchanter(itemId) {
|
||||
const item = this.items.get(itemId)
|
||||
if (!item) return
|
||||
if (!["arme", "equipement", "protection", "bouclier"].includes(item.type)) {
|
||||
ui.notifications.warn("Seules les armes, équipements, protections et boucliers peuvent être enchantés.")
|
||||
return
|
||||
}
|
||||
await MournbladeEnchantementDialog.create(this, item)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
@@ -642,8 +857,7 @@ export class MournbladeActor extends Actor {
|
||||
}
|
||||
console.log("ARME!", rollData)
|
||||
this.depenseRessources(arme)
|
||||
let rollDialog = await MournbladeRollDialog.create(this, rollData)
|
||||
rollDialog.render(true)
|
||||
await MournbladeRollDialog.create(this, rollData)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
@@ -657,8 +871,7 @@ export class MournbladeActor extends Actor {
|
||||
rollData.difficulte = rollData.defender.system.attributs.tre.value * 2
|
||||
}
|
||||
console.log("Assomer!", rollData)
|
||||
let rollDialog = await MournbladeRollDialog.create(this, rollData)
|
||||
rollDialog.render(true)
|
||||
await MournbladeRollDialog.create(this, rollData)
|
||||
}
|
||||
/* -------------------------------------------- */
|
||||
async rollFuir() {
|
||||
@@ -672,8 +885,7 @@ export class MournbladeActor extends Actor {
|
||||
rollData.difficulte = rollData.defender.system.attributs.adr.value + ((comp) ? comp.system.niveau : rollData.defender.system.attributs.adr.value)
|
||||
}
|
||||
console.log("Fuir!", rollData)
|
||||
let rollDialog = await MournbladeRollDialog.create(this, rollData)
|
||||
rollDialog.render(true)
|
||||
await MournbladeRollDialog.create(this, rollData)
|
||||
}
|
||||
/* -------------------------------------------- */
|
||||
async rollImmobiliser() {
|
||||
@@ -686,15 +898,14 @@ export class MournbladeActor extends Actor {
|
||||
rollData.difficulte = rollData.defenderCombatValues.defenseTotal
|
||||
}
|
||||
console.log("Immobiliser!", rollData)
|
||||
let rollDialog = await MournbladeRollDialog.create(this, rollData)
|
||||
rollDialog.render(true)
|
||||
await MournbladeRollDialog.create(this, rollData)
|
||||
}
|
||||
/* -------------------------------------------- */
|
||||
async rollArmeSpecial(armeId) {
|
||||
let arme = this.items.get(armeId)
|
||||
if (arme) {
|
||||
MournbladeUtility.createChatWithRollMode("GM", {
|
||||
content: await renderTemplate(`systems/fvtt-mournblade/templates/chat-display-description.html`, arme)
|
||||
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-mournblade/templates/chat-display-description.hbs`, arme)
|
||||
}, arme)
|
||||
this.depenseRessources(arme)
|
||||
}
|
||||
@@ -710,9 +921,17 @@ export class MournbladeActor extends Actor {
|
||||
arme = this.prepareBouclier(arme)
|
||||
}
|
||||
//Unused rollData.degatsFormula = arme.system.totalDegats
|
||||
let roll = await new Roll(arme.system.totalDegats).roll()
|
||||
let roll
|
||||
try {
|
||||
roll = await new Roll(arme.system.totalDegats).evaluate()
|
||||
} catch (err) {
|
||||
ui.notifications.error(`Formule de dégâts invalide : ${arme.system.totalDegats}`)
|
||||
console.error("rollArmeDegats error:", err)
|
||||
return
|
||||
}
|
||||
await MournbladeUtility.showDiceSoNice(roll, game.settings.get("core", "rollMode"));
|
||||
let rollData = {
|
||||
degatsFormula: arme.system.totalDegats,
|
||||
arme: arme,
|
||||
finalResult: roll.total,
|
||||
alias: this.name,
|
||||
@@ -721,7 +940,7 @@ export class MournbladeActor extends Actor {
|
||||
actionImg: arme.img,
|
||||
}
|
||||
MournbladeUtility.createChatWithRollMode(rollData.alias, {
|
||||
content: await renderTemplate(`systems/fvtt-mournblade/templates/chat-degats-result.html`, rollData)
|
||||
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-mournblade/templates/chat-degats-result-v2.hbs`, rollData)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export class MournbladeCombat extends Combat {
|
||||
const c = this.combatants.get(ids[cId]);
|
||||
let id = c._id || c.id;
|
||||
let initBonus = c.actor ? c.actor.getInitiativeScore() : 0
|
||||
let roll = new Roll("1d10 + "+initBonus).roll({ async: false})
|
||||
let roll = await new Roll("1d10 + "+initBonus).evaluate()
|
||||
await MournbladeUtility.showDiceSoNice(roll, game.settings.get("core", "rollMode"))
|
||||
//console.log("Init bonus", initBonus, roll.total)
|
||||
await this.updateEmbeddedDocuments("Combatant", [ { _id: id, initiative: roll.total } ]);
|
||||
|
||||
@@ -89,7 +89,7 @@ export class MournbladeCommands {
|
||||
if (command && command.func) {
|
||||
const result = command.func(content, msg, params);
|
||||
if (result == false) {
|
||||
RdDCommands._chatAnswer(msg, command.descr);
|
||||
MournbladeCommands._chatAnswer(msg, command.descr);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -98,8 +98,8 @@ export class MournbladeCommands {
|
||||
|
||||
/* -------------------------------------------- */
|
||||
async createChar(msg) {
|
||||
game.system.Mournblade.creator = new MournbladeActorCreate();
|
||||
game.system.Mournblade.creator.start();
|
||||
game.system.mournblade.creator = new MournbladeActorCreate();
|
||||
game.system.mournblade.creator.start();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
@@ -116,8 +116,7 @@ export class MournbladeCommands {
|
||||
rollData.mode = "generic"
|
||||
rollData.title = `Dice Pool Roll`;
|
||||
|
||||
let rollDialog = await MournbladeRollDialog.create( this, rollData);
|
||||
rollDialog.render( true );
|
||||
await MournbladeRollDialog.create( this, rollData);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -72,6 +72,74 @@ export class MournbladeConfig {
|
||||
loi: game.i18n.localize("MNBL.law"),
|
||||
betes: game.i18n.localize("MNBL.beastslords"),
|
||||
elementaires: game.i18n.localize("MNBL.elementslords")
|
||||
},
|
||||
potionFormeOptions: {
|
||||
liquide: game.i18n.localize("MNBL.potionLiquide"),
|
||||
onguent: game.i18n.localize("MNBL.potionOnguent"),
|
||||
cachets: game.i18n.localize("MNBL.potionCachets"),
|
||||
pilules: game.i18n.localize("MNBL.potionPilules"),
|
||||
},
|
||||
potionStatutOptions: {
|
||||
inconnue: game.i18n.localize("MNBL.potionInconnue"),
|
||||
efficace: game.i18n.localize("MNBL.potionEfficace"),
|
||||
heroique: game.i18n.localize("MNBL.potionHeroique"),
|
||||
inefficace: game.i18n.localize("MNBL.potionInnefficace"),
|
||||
poison: game.i18n.localize("MNBL.potionPoison"),
|
||||
},
|
||||
typeCapaciteOptions: {
|
||||
elue: game.i18n.localize("MNBL.typeCapaciteElue"),
|
||||
elementaire: game.i18n.localize("MNBL.typeCapaciteElementaire"),
|
||||
demoniaque: game.i18n.localize("MNBL.typeCapaciteDemoniaque"),
|
||||
automaton: game.i18n.localize("MNBL.typeCapaciteAutomaton"),
|
||||
creature: game.i18n.localize("MNBL.typeCapaciteCreature"),
|
||||
faiblessedemoniaque: game.i18n.localize("MNBL.typeCapaciteFaiblesseDemoniaque"),
|
||||
},
|
||||
creatureTypeOptions: {
|
||||
creature: game.i18n.localize("MNBL.creatureTypeCreature"),
|
||||
demon: game.i18n.localize("MNBL.creatureTypeDemon"),
|
||||
elementaire: game.i18n.localize("MNBL.creatureTypeElementaire"),
|
||||
automaton: game.i18n.localize("MNBL.creatureTypeAutomaton"),
|
||||
},
|
||||
demonTypeOptions: {
|
||||
"": game.i18n.localize("MNBL.demonTypeNone"),
|
||||
combat: game.i18n.localize("MNBL.demonTypeCombat"),
|
||||
desir: game.i18n.localize("MNBL.demonTypeDesir"),
|
||||
savoir: game.i18n.localize("MNBL.demonTypeSavoir"),
|
||||
protection: game.i18n.localize("MNBL.demonTypeProtection"),
|
||||
voyage: game.i18n.localize("MNBL.demonTypeVoyage"),
|
||||
},
|
||||
demonPuissanceOptions: {
|
||||
"": game.i18n.localize("MNBL.demonPuissanceNone"),
|
||||
mineur: game.i18n.localize("MNBL.demonPuissanceMineur"),
|
||||
median: game.i18n.localize("MNBL.demonPuissanceMedian"),
|
||||
majeur: game.i18n.localize("MNBL.demonPuissanceMajeur"),
|
||||
},
|
||||
elementTypeOptions: {
|
||||
air: game.i18n.localize("MNBL.elementTypeAir"),
|
||||
terre: game.i18n.localize("MNBL.elementTypeTerre"),
|
||||
feu: game.i18n.localize("MNBL.elementTypeFeu"),
|
||||
eau: game.i18n.localize("MNBL.elementTypeEau"),
|
||||
},
|
||||
automatonTypeOptions: {
|
||||
"": game.i18n.localize("MNBL.automatonTypeNone"),
|
||||
combat: game.i18n.localize("MNBL.automatonTypeCombat"),
|
||||
voyage: game.i18n.localize("MNBL.automatonTypeVoyage"),
|
||||
perception: game.i18n.localize("MNBL.automatonTypePerception"),
|
||||
restauration: game.i18n.localize("MNBL.automatonTypeRestauration"),
|
||||
reparateur: game.i18n.localize("MNBL.automatonTypeReparateur"),
|
||||
},
|
||||
automatonPuissanceOptions: {
|
||||
"": game.i18n.localize("MNBL.automatonPuissanceNone"),
|
||||
mineur: game.i18n.localize("MNBL.automatonPuissanceMineur"),
|
||||
median: game.i18n.localize("MNBL.automatonPuissanceMedian"),
|
||||
majeur: game.i18n.localize("MNBL.automatonPuissanceMajeur"),
|
||||
},
|
||||
automatonVoyageTypeOptions: {
|
||||
"": game.i18n.localize("MNBL.automatonVoyageTypeNone"),
|
||||
terrestre: game.i18n.localize("MNBL.automatonVoyageTypeTerrestre"),
|
||||
aquatique: game.i18n.localize("MNBL.automatonVoyageTypeAquatique"),
|
||||
aerien: game.i18n.localize("MNBL.automatonVoyageTypeAerien"),
|
||||
extradimensionnel: game.i18n.localize("MNBL.automatonVoyageTypeExtradimensionnel"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export class MournbladeItemSheet extends foundry.appv1.sheets.ItemSheet {
|
||||
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
classes: ["fvtt-mournblade", "sheet", "item"],
|
||||
template: "systems/fvtt-mournblade/templates/item-sheet.html",
|
||||
template: "systems/fvtt-mournblade/templates/item-sheet.hbs",
|
||||
dragDrop: [{ dragSelector: null, dropSelector: null }],
|
||||
width: 620,
|
||||
height: 550
|
||||
@@ -106,7 +106,7 @@ export class MournbladeItemSheet extends foundry.appv1.sheets.ItemSheet {
|
||||
payload: chatData,
|
||||
});
|
||||
|
||||
renderTemplate('systems/fvtt-Mournblade-rpg/templates/post-item.html', chatData).then(html => {
|
||||
foundry.applications.handlebars.renderTemplate('systems/fvtt-mournblade/templates/post-item.hbs', chatData).then(html => {
|
||||
let chatOptions = MournbladeUtility.chatDataSetup(html);
|
||||
ChatMessage.create(chatOptions)
|
||||
});
|
||||
@@ -169,7 +169,7 @@ export class MournbladeItemSheet extends foundry.appv1.sheets.ItemSheet {
|
||||
/* -------------------------------------------- */
|
||||
get template() {
|
||||
let type = this.item.type;
|
||||
return `systems/fvtt-mournblade/templates/item-${type}-sheet.html`;
|
||||
return `systems/fvtt-mournblade/templates/item-${type}-sheet.hbs`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
@@ -12,9 +12,10 @@ export const defaultItemImg = {
|
||||
protection: "systems/fvtt-mournblade/assets/icons/protection.webp",
|
||||
rune: "systems/fvtt-mournblade/assets/icons/rune.webp",
|
||||
runeeffect: "systems/fvtt-mournblade/assets/icons/rune.webp",
|
||||
potion: "systems/fvtt-mournblade/assets/icons/potion.webp",
|
||||
tendance: "systems/fvtt-mournblade/assets/icons/tendance.webp",
|
||||
traitchaotique: "systems/fvtt-mournblade/assets/icons/traitchaotique.webp",
|
||||
traitespece: "systems/fvtt-mournblade/assets/icons/capacite.webp"
|
||||
traitespece: "systems/fvtt-mournblade/assets/icons/capacite.webp",
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,13 +10,20 @@
|
||||
// Import Modules
|
||||
import { MournbladeActor } from "./mournblade-actor.js";
|
||||
import { MournbladeItemSheet } from "./mournblade-item-sheet.js";
|
||||
import { MournbladeActorSheet } from "./mournblade-actor-sheet.js";
|
||||
import { MournbladeCreatureSheet } from "./mournblade-creature-sheet.js";
|
||||
// import { MournbladeActorSheet } from "./mournblade-actor-sheet.js";
|
||||
// import { MournbladeCreatureSheet } from "./mournblade-creature-sheet.js";
|
||||
import { MournbladeUtility } from "./mournblade-utility.js";
|
||||
import { MournbladeCombat } from "./mournblade-combat.js";
|
||||
import { MournbladeItem } from "./mournblade-item.js";
|
||||
import { MournbladeConfig } from "./mournblade-config.js";
|
||||
|
||||
// Import DataModels
|
||||
import * as models from "./models/index.mjs";
|
||||
|
||||
// Import AppV2 Item Sheets
|
||||
import * as sheets from "./applications/sheets/_module.mjs";
|
||||
import { MournbladeRollDialog } from "./mournblade-roll-dialog.js";
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Foundry VTT Initialization */
|
||||
/* -------------------------------------------- */
|
||||
@@ -37,7 +44,7 @@ Hooks.once("init", async function () {
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
game.socket.on("system.fvtt-mournblade-rpg", data => {
|
||||
game.socket.on("system.fvtt-mournblade", data => {
|
||||
MournbladeUtility.onSocketMesssage(data);
|
||||
});
|
||||
|
||||
@@ -45,35 +52,82 @@ Hooks.once("init", async function () {
|
||||
// Define custom Entity classes
|
||||
CONFIG.Combat.documentClass = MournbladeCombat
|
||||
CONFIG.Actor.documentClass = MournbladeActor
|
||||
CONFIG.Actor.dataModels = {
|
||||
personnage: models.PersonnageDataModel,
|
||||
creature: models.CreatureDataModel,
|
||||
}
|
||||
|
||||
CONFIG.Item.documentClass = MournbladeItem
|
||||
CONFIG.Item.dataModels = {
|
||||
arme: models.ArmeDataModel,
|
||||
bouclier: models.BouclierDataModel,
|
||||
capacite: models.CapaciteDataModel,
|
||||
competence: models.CompetenceDataModel,
|
||||
don: models.DonDataModel,
|
||||
equipement: models.EquipementDataModel,
|
||||
heritage: models.HeritageDataModel,
|
||||
metier: models.MetierDataModel,
|
||||
modifier: models.ModifierDataModel,
|
||||
monnaie: models.MonnaieDataModel,
|
||||
origine: models.OrigineDataModel,
|
||||
pacte: models.PacteDataModel,
|
||||
protection: models.ProtectionDataModel,
|
||||
rune: models.RuneDataModel,
|
||||
runeeffect: models.RuneEffectDataModel,
|
||||
potion: models.PotionDataModel,
|
||||
tendance: models.TendanceDataModel,
|
||||
traitchaotique: models.TraitChaotiqueDataModel,
|
||||
traitespece: models.TraitEspeceDataModel
|
||||
}
|
||||
|
||||
game.system.mournblade = {
|
||||
config : MournbladeConfig.getConfig(),
|
||||
models,
|
||||
sheets,
|
||||
MournbladeRollDialog
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
// Register sheet application classes
|
||||
foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet);
|
||||
foundry.documents.collections.Actors.registerSheet("fvtt-mournblade", MournbladeActorSheet, { types: ["personnage"], makeDefault: true })
|
||||
foundry.documents.collections.Actors.registerSheet("fvtt-mournblade", MournbladeCreatureSheet, { types: ["creature"], makeDefault: true })
|
||||
foundry.documents.collections.Actors.registerSheet("fvtt-mournblade", sheets.MournbladePersonnageSheet, { types: ["personnage"], makeDefault: true });
|
||||
foundry.documents.collections.Actors.registerSheet("fvtt-mournblade", sheets.MournbladeCreatureSheet, { types: ["creature"], makeDefault: true });
|
||||
|
||||
foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ItemSheet);
|
||||
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", MournbladeItemSheet, { makeDefault: true })
|
||||
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeArmeSheet, { types: ["arme"], makeDefault: true });
|
||||
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeBouclierSheet, { types: ["bouclier"], makeDefault: true });
|
||||
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeCapaciteSheet, { types: ["capacite"], makeDefault: true });
|
||||
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeCompetenceSheet, { types: ["competence"], makeDefault: true });
|
||||
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeDonSheet, { types: ["don"], makeDefault: true });
|
||||
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeEquipementSheet, { types: ["equipement"], makeDefault: true });
|
||||
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeHeritageSheet, { types: ["heritage"], makeDefault: true });
|
||||
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeMetierSheet, { types: ["metier"], makeDefault: true });
|
||||
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeModifierSheet, { types: ["modifier"], makeDefault: true });
|
||||
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeMonnaieSheet, { types: ["monnaie"], makeDefault: true });
|
||||
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeOrigineSheet, { types: ["origine"], makeDefault: true });
|
||||
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladePacteSheet, { types: ["pacte"], makeDefault: true });
|
||||
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeProtectionSheet, { types: ["protection"], makeDefault: true });
|
||||
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeRuneSheet, { types: ["rune"], makeDefault: true });
|
||||
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeRuneEffectSheet, { types: ["runeeffect"], makeDefault: true });
|
||||
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladePotionSheet, { types: ["potion"], makeDefault: true });
|
||||
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeTendanceSheet, { types: ["tendance"], makeDefault: true });
|
||||
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeTraitChaotiqueSheet, { types: ["traitchaotique"], makeDefault: true });
|
||||
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeTraitEspeceSheet, { types: ["traitespece"], makeDefault: true });
|
||||
|
||||
MournbladeUtility.init();
|
||||
|
||||
});
|
||||
|
||||
/* -------------------------------------------- */
|
||||
function welcomeMessage() {
|
||||
ChatMessage.create({
|
||||
async function welcomeMessage() {
|
||||
const templateData = {};
|
||||
const html = await foundry.applications.handlebars.renderTemplate("systems/fvtt-mournblade/templates/chat-welcome-message.hbs", templateData);
|
||||
|
||||
await ChatMessage.create({
|
||||
user: game.user.id,
|
||||
whisper: [game.user.id],
|
||||
content: `<div id="welcome-message-Mournblade"><span class="rdd-roll-part">
|
||||
<strong>Bienvenue dans les Jeunes Royaumes de Mournblade !</strong>
|
||||
<p>Les livres de Mournblade sont nécessaires pour jouer : https://www.titam-france.fr</p>
|
||||
<p>Mournblade est jeu de rôle publié par Titam France/Sombres projets, tout les droits leur appartiennent.</p>
|
||||
<p>Système développé par LeRatierBretonnien, support sur le <a href="https://discord.gg/pPSDNJk">Discord FR de Foundry</a>.</p>
|
||||
` });
|
||||
content: html
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
@@ -90,7 +144,7 @@ async function importDefaultScene() {
|
||||
/* -------------------------------------------- */
|
||||
/* Foundry VTT Initialization */
|
||||
/* -------------------------------------------- */
|
||||
Hooks.once("ready", function () {
|
||||
Hooks.once("ready", async function () {
|
||||
|
||||
game.system.mournblade = {
|
||||
config : MournbladeConfig.getConfig(),
|
||||
@@ -100,16 +154,16 @@ Hooks.once("ready", function () {
|
||||
// User warning
|
||||
if (!game.user.isGM && game.user.character == undefined) {
|
||||
ui.notifications.info("Attention ! Aucun personnage n'est relié au joueur !");
|
||||
ChatMessage.create({
|
||||
await ChatMessage.create({
|
||||
content: "<b>ATTENTION</b> Le joueur " + game.user.name + " n'est relié à aucun personnage !",
|
||||
user: game.user._id
|
||||
user: game.user.id
|
||||
});
|
||||
}
|
||||
if (!game.user.isGM && game.user.character && !game.user.character.prototypeToken.actorLink) {
|
||||
ui.notifications.info("Le token de du joueur n'est pas connecté à l'acteur !");
|
||||
ChatMessage.create({
|
||||
await ChatMessage.create({
|
||||
content: "<b>ATTENTION</b> Le token du joueur " + game.user.name + " n'est pas connecté à l'acteur !",
|
||||
user: game.user._id
|
||||
user: game.user.id
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
import { MournbladeUtility } from "./mournblade-utility.js";
|
||||
|
||||
export class MournbladeRollDialog extends Dialog {
|
||||
export class MournbladeRollDialog extends Application {
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static async create(actor, rollData ) {
|
||||
|
||||
let options = { classes: ["MournbladeDialog"], width: 340, height: 'fit-content', 'z-index': 99999 };
|
||||
let html = await foundry.applications.handlebars.renderTemplate('systems/fvtt-mournblade/templates/roll-dialog-generic.html', rollData);
|
||||
|
||||
return new MournbladeRollDialog(actor, rollData, html, options );
|
||||
return new MournbladeRollDialog(actor, rollData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
constructor(actor, rollData, html, options, close = undefined) {
|
||||
let conf = {
|
||||
title: "Test de Capacité",
|
||||
content: html,
|
||||
buttons: {
|
||||
rolld10: {
|
||||
icon: '<i class="fas fa-check"></i>',
|
||||
label: "Lancer 1d10",
|
||||
callback: () => { this.roll("1d10") }
|
||||
},
|
||||
rolld20: {
|
||||
icon: '<i class="fas fa-check"></i>',
|
||||
label: "Lancer 1d20",
|
||||
callback: () => { this.roll("1d20") }
|
||||
},
|
||||
cancel: {
|
||||
icon: '<i class="fas fa-times"></i>',
|
||||
label: "Annuler",
|
||||
callback: () => { this.close() }
|
||||
} },
|
||||
close: close
|
||||
constructor(actor, rollData, options = {}) {
|
||||
super(options);
|
||||
this.actor = actor;
|
||||
this.rollData = rollData;
|
||||
}
|
||||
|
||||
super(conf, options);
|
||||
static get defaultOptions() {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
classes: ["fvtt-mournblade", "sheet", "item"],
|
||||
template: "systems/fvtt-mournblade/templates/roll-dialog-generic.hbs",
|
||||
width: 400,
|
||||
height: "auto",
|
||||
title: "Test de Capacité"
|
||||
});
|
||||
}
|
||||
|
||||
this.actor = actor
|
||||
this.rollData = rollData
|
||||
getData() {
|
||||
const data = foundry.utils.duplicate(this.rollData);
|
||||
if (!data.config) {
|
||||
data.config = game.system.mournblade.config;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
_onCancel() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
roll ( dice) {
|
||||
this.rollData.mainDice = dice
|
||||
MournbladeUtility.rollMournblade( this.rollData )
|
||||
_onRoll(dice) {
|
||||
this.rollData.mainDice = dice;
|
||||
MournbladeUtility.rollMournblade(this.rollData);
|
||||
this.close();
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
|
||||
// Roll buttons
|
||||
html.find(".rolld10").click(this._onRoll.bind(this, "1d10"));
|
||||
html.find(".rolld20").click(this._onRoll.bind(this, "1d20"));
|
||||
html.find(".cancel").click(this._onCancel.bind(this));
|
||||
|
||||
function onLoad() {
|
||||
}
|
||||
$(function () { onLoad(); });
|
||||
|
||||
@@ -127,6 +127,12 @@ export class MournbladeUtility {
|
||||
/* -------------------------------------------- */
|
||||
static async chatListeners(html) {
|
||||
|
||||
$(html).on("click", '.mournblade-open-guide', async event => {
|
||||
event.preventDefault()
|
||||
const doc = await fromUuid("Compendium.fvtt-mournblade.journal-aide.JournalEntry.JurnlHelpGuide01")
|
||||
if (doc) doc.sheet.render(true)
|
||||
})
|
||||
|
||||
$(html).on("click", '.predilection-reroll', async event => {
|
||||
let predIdx = $(event.currentTarget).data("predilection-index")
|
||||
let messageId = MournbladeUtility.findChatMessageId(event.currentTarget)
|
||||
@@ -154,17 +160,74 @@ export class MournbladeUtility {
|
||||
if (game.user.isGM) {
|
||||
MournbladeUtility.applyDegatsFromAttaque(rollData)
|
||||
} else {
|
||||
game.socket.emit("system.fvtt-mournblade", { name: "msg_apply_damage", data: { rolLData: rollData } })
|
||||
game.socket.emit("system.fvtt-mournblade", { name: "msg_apply_damage", data: { rollData: rollData } })
|
||||
}
|
||||
})
|
||||
|
||||
$(html).on("click", '.rune-post-chat', async event => {
|
||||
event.preventDefault()
|
||||
const btn = event.currentTarget
|
||||
const actorId = btn.dataset.actorId
|
||||
const itemId = btn.dataset.itemId
|
||||
await MournbladeUtility.postItemToChat(actorId, itemId)
|
||||
})
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static async postItemToChat(actorId, itemId) {
|
||||
const actor = game.actors.get(actorId)
|
||||
const item = actor?.items.get(itemId)
|
||||
if (!item) { ui.notifications.warn("Item introuvable."); return }
|
||||
|
||||
let chatData = foundry.utils.duplicate(item)
|
||||
if (actor) chatData.actor = { id: actor.id }
|
||||
if (chatData.img?.includes("/blank.png")) chatData.img = null
|
||||
|
||||
chatData.jsondata = JSON.stringify({ compendium: "postedItem", payload: chatData })
|
||||
|
||||
const typeLabels = {
|
||||
arme: "Arme", bouclier: "Bouclier", competence: "Compétence",
|
||||
rune: "Rune", runeeffect: "Rune Active", don: "Don", pacte: "Pacte",
|
||||
protection: "Protection", equipement: "Équipement", heritage: "Héritage",
|
||||
metier: "Métier", capacite: "Capacité", tendance: "Tendance",
|
||||
traitchaotique: "Trait Chaotique", traitespece: "Trait d'Espèce",
|
||||
origine: "Origine", modifier: "Modificateur", monnaie: "Monnaie"
|
||||
}
|
||||
chatData.typeLabel = typeLabels[chatData.type] ?? chatData.type
|
||||
|
||||
const typeIcons = {
|
||||
arme: "fa-sword", bouclier: "fa-shield-halved", competence: "fa-graduation-cap",
|
||||
rune: "fa-star-of-david", runeeffect: "fa-star-of-david", don: "fa-hand-sparkles",
|
||||
pacte: "fa-scroll", protection: "fa-shield", equipement: "fa-box",
|
||||
heritage: "fa-dna", metier: "fa-hammer", capacite: "fa-bolt",
|
||||
tendance: "fa-yin-yang", traitchaotique: "fa-skull", traitespece: "fa-paw",
|
||||
origine: "fa-compass", modifier: "fa-sliders", monnaie: "fa-coins",
|
||||
potion: "fa-flask"
|
||||
}
|
||||
chatData.typeIcon = typeIcons[chatData.type] ?? "fa-cube"
|
||||
|
||||
const html = await foundry.applications.handlebars.renderTemplate(
|
||||
'systems/fvtt-mournblade/templates/post-item.hbs', chatData)
|
||||
ChatMessage.create({ user: game.user.id, content: html })
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static async preloadHandlebarsTemplates() {
|
||||
|
||||
const templatePaths = [
|
||||
'systems/fvtt-mournblade/templates/editor-notes-gm.html',
|
||||
'systems/fvtt-mournblade/templates/partial-item-description.html'
|
||||
'systems/fvtt-mournblade/templates/editor-notes-gm.hbs',
|
||||
'systems/fvtt-mournblade/templates/partial-item-description.hbs',
|
||||
'systems/fvtt-mournblade/templates/partial-item-header.hbs',
|
||||
'systems/fvtt-mournblade/templates/partial-item-nav.hbs',
|
||||
'systems/fvtt-mournblade/templates/partial-item-enchantement.hbs',
|
||||
'systems/fvtt-mournblade/templates/dialog-invocation-elementaire.hbs',
|
||||
'systems/fvtt-mournblade/templates/chat-invocation-result.hbs',
|
||||
'systems/fvtt-mournblade/templates/dialog-invocation-demon.hbs',
|
||||
'systems/fvtt-mournblade/templates/chat-invocation-demon-result.hbs',
|
||||
'systems/fvtt-mournblade/templates/dialog-enchantement.hbs',
|
||||
'systems/fvtt-mournblade/templates/chat-enchantement-result.hbs',
|
||||
'systems/fvtt-mournblade/templates/dialog-invocation-esprit.hbs',
|
||||
'systems/fvtt-mournblade/templates/chat-invocation-esprit-result.hbs',
|
||||
]
|
||||
return foundry.applications.handlebars.loadTemplates(templatePaths);
|
||||
}
|
||||
@@ -209,7 +272,7 @@ export class MournbladeUtility {
|
||||
static createArrayOptionList(min, max) {
|
||||
let options = [];
|
||||
for (let i = min; i <= max; i++) {
|
||||
options.push({key:`${i}`, label:`${i}`});
|
||||
options.push({ key: `${i}`, label: `${i}` });
|
||||
}
|
||||
return options;
|
||||
}
|
||||
@@ -292,13 +355,18 @@ export class MournbladeUtility {
|
||||
if (diceValue % 2 == 1) {
|
||||
//console.log("PAIR/IMP2", diceValue)
|
||||
rollData.finalResult -= rollData.roll.terms[0].results[0].result // Substract value
|
||||
rollData.isImpair = true
|
||||
if (diceValue == 1 || diceValue == 11) {
|
||||
rollData.isDramatique = true
|
||||
rollData.isSuccess = false
|
||||
}
|
||||
}
|
||||
}
|
||||
this.computeQualityResult(rollData)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static computeQualityResult(rollData) {
|
||||
//console.log("Result : ", rollData)
|
||||
if (rollData.difficulte > 0 && !rollData.isDramatique) {
|
||||
rollData.isSuccess = (rollData.finalResult >= rollData.difficulte)
|
||||
@@ -306,6 +374,7 @@ export class MournbladeUtility {
|
||||
rollData.isDramatique = ((rollData.finalResult - rollData.difficulte) <= -10)
|
||||
rollData.isPureSuccess = (rollData.isSuccess && !rollData.isHeroique)
|
||||
}
|
||||
rollData.isEchec = !rollData.isSuccess
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
@@ -367,7 +436,7 @@ export class MournbladeUtility {
|
||||
if (rollData.visee) {
|
||||
rollData.diceFormula += "+5"
|
||||
}
|
||||
if (rollData.cibleconsciente) {
|
||||
if (rollData.cibleconsciente && rollData.defender) {
|
||||
rollData.diceFormula += `-${rollData.defender.system.attributs.adr.value}`
|
||||
}
|
||||
if (rollData.ciblecourt) {
|
||||
@@ -377,18 +446,22 @@ export class MournbladeUtility {
|
||||
rollData.diceFormula += `-10`
|
||||
}
|
||||
}
|
||||
if (rollData.typeCouvert != "aucun") {
|
||||
if (rollData.typeCouvert && rollData.typeCouvert != "aucun" && rollData.config.couverts[rollData.typeCouvert]) {
|
||||
rollData.diceFormula += `+${rollData.config.couverts[rollData.typeCouvert].value}`
|
||||
}
|
||||
}
|
||||
|
||||
if (rollData.rune) {
|
||||
rollData.runeduree = Math.ceil((rollData.runeame + 3) / 3)
|
||||
const actionsBase = Math.ceil(rollData.runeame / 3)
|
||||
rollData.runeActionsComplexes = (rollData.runemode == "inscrire") ? actionsBase * 2 : actionsBase
|
||||
|
||||
if (rollData.runemode == "inscrire") {
|
||||
rollData.runeduree *= 2
|
||||
}
|
||||
if (rollData.runemode == "prononcer") {
|
||||
rollData.runeduree = 1
|
||||
rollData.runeduree = null // durée infinie
|
||||
rollData.dureeLabel = "infinie"
|
||||
} else {
|
||||
// prononcer : 1 heure de base + 1 heure par tranche de 2 points d'âme
|
||||
rollData.runeduree = 1 + Math.floor(rollData.runeame / 2)
|
||||
rollData.dureeLabel = rollData.runeduree === 1 ? "1 heure" : `${rollData.runeduree} heures`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,10 +477,52 @@ export class MournbladeUtility {
|
||||
// Application immédiate selon type de jet
|
||||
if (rollData.rune) {
|
||||
let subAme = rollData.runeame
|
||||
if (rollData.isEchec && !rollData.isDramatique) {
|
||||
|
||||
// Réussite héroïque + rune uniquement sur soi : coût d'âme divisé par 2 (arrondi sup.)
|
||||
if (rollData.isHeroique && rollData.runeautocible) {
|
||||
subAme = Math.ceil(subAme / 2)
|
||||
rollData.runeameCostReduit = true
|
||||
rollData.runeameCostFinal = subAme
|
||||
} else if (rollData.isEchec && !rollData.isDramatique) {
|
||||
// Échec simple : perd la moitié (arrondie sup.)
|
||||
subAme = Math.ceil((subAme + 1) / 2)
|
||||
}
|
||||
actor.subPointsAme(rollData.runemode, subAme)
|
||||
|
||||
// Échec dramatique : dé du Chaos (d20)
|
||||
if (rollData.isDramatique) {
|
||||
const chaosRoll = await new Roll("1d20").evaluate()
|
||||
await this.showDiceSoNice(chaosRoll, game.settings.get("core", "rollMode"))
|
||||
const cr = chaosRoll.terms[0].results[0].result
|
||||
rollData.chaosDieResult = cr
|
||||
const claValue = rollData.attr?.value ?? 0
|
||||
if (cr === 1 || cr === 11) {
|
||||
rollData.chaosEffet = "desastre"
|
||||
rollData.chaosEffetTexte = `Désastre extraordinaire ! La Rune se déclenche à des kilomètres de là sur des cibles inconnues. La Loi se manifeste : le sorcier ne peut plus utiliser l'Œil pendant ${claValue} semaine${claValue > 1 ? "s" : ""}.`
|
||||
} else if (cr % 2 === 1) {
|
||||
rollData.chaosEffet = "echec_absolu"
|
||||
rollData.chaosEffetTexte = "Échec absolu. Le MJ décide si la Rune se manifeste sur des cibles autres, dans des proportions désavantageuses ou en un lieu très lointain."
|
||||
} else {
|
||||
rollData.chaosEffet = "rien"
|
||||
rollData.chaosEffetTexte = "Rien de particulier ne se produit en plus de la perte des points d'Âme."
|
||||
}
|
||||
}
|
||||
|
||||
// Créer l'effet de rune sur l'acteur si le jet est réussi
|
||||
if (rollData.isSuccess) {
|
||||
const effetMode = (rollData.runemode == "prononcer") ? "prononcee" : "inscrite"
|
||||
await actor.createEmbeddedDocuments("Item", [{
|
||||
name: rollData.rune.name,
|
||||
type: "runeeffect",
|
||||
img: rollData.rune.img || "systems/fvtt-mournblade/assets/icons/rune.webp",
|
||||
system: {
|
||||
rune: rollData.rune.name,
|
||||
mode: effetMode,
|
||||
duree: rollData.dureeLabel,
|
||||
pointame: rollData.runeame
|
||||
}
|
||||
}])
|
||||
}
|
||||
}
|
||||
if (rollData.typeAttaque == "assomer" && rollData.defenderTokenId && rollData.isPureSuccess) {
|
||||
let defender = game.canvas.tokens.get(rollData?.defenderTokenId)?.actor
|
||||
@@ -423,11 +538,104 @@ export class MournbladeUtility {
|
||||
actor.setModifier("Charge de Cavalerie : -5 défense pour le tour", "defense", -5)
|
||||
}
|
||||
this.createChatWithRollMode(rollData.alias, {
|
||||
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-mournblade/templates/chat-generic-result.html`, rollData)
|
||||
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-mournblade/templates/chat-generic-result-v2.hbs`, rollData)
|
||||
}, rollData)
|
||||
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static async rollSortilege(rollData) {
|
||||
if (!rollData.sortilegeRunes || rollData.sortilegeRunes.length === 0) {
|
||||
ui.notifications.warn("Aucune Rune sélectionnée pour le Sortilège.")
|
||||
return
|
||||
}
|
||||
|
||||
const actor = rollData.tokenId
|
||||
? game.canvas.tokens.get(rollData.tokenId)?.actor
|
||||
: game.actors.get(rollData.actorId)
|
||||
|
||||
// Pré-calcul des infos du sortilège
|
||||
const isInscrire = rollData.runemode === "inscrire"
|
||||
|
||||
rollData.sortilegeRunes.forEach(r => {
|
||||
r.actionsComplexes = Math.ceil(r.pts / 3) * (isInscrire ? 2 : 1)
|
||||
if (isInscrire) {
|
||||
r.dureeLabel = "infinie"
|
||||
} else {
|
||||
const h = 1 + Math.floor(r.pts / 2)
|
||||
r.dureeLabel = h === 1 ? "1 heure" : `${h} heures`
|
||||
}
|
||||
})
|
||||
|
||||
rollData.runeActionsComplexes = rollData.sortilegeRunes.reduce((s, r) => s + r.actionsComplexes, 0)
|
||||
|
||||
// Construction de la formule de jet : mainDice + CLA + Savoir:Runes + malus + modificateur
|
||||
const compNiveau = rollData.competence?.system?.niveau ?? 0
|
||||
const compMod = compNiveau === 0 ? -3 : 0
|
||||
rollData.diceFormula = `${rollData.mainDice}+${rollData.attr.value}+${compNiveau}+${rollData.modificateur}+${compMod}+${rollData.malusSante}+${rollData.malusAme}`
|
||||
|
||||
const myRoll = await new Roll(rollData.diceFormula).evaluate()
|
||||
await this.showDiceSoNice(myRoll, game.settings.get("core", "rollMode"))
|
||||
rollData.roll = foundry.utils.duplicate(myRoll)
|
||||
rollData.diceResult = myRoll.terms[0].results[0].result
|
||||
rollData.finalResult = myRoll.total
|
||||
this.computeResult(rollData)
|
||||
|
||||
// Déduction des points d'âme
|
||||
let totalCost = rollData.sortilegeRunes.reduce((s, r) => s + r.pts, 0)
|
||||
if (rollData.isHeroique && rollData.runeautocible) {
|
||||
totalCost = Math.ceil(totalCost / 2)
|
||||
rollData.runeameCostReduit = true
|
||||
rollData.runeameCostFinal = totalCost
|
||||
} else if (rollData.isEchec && !rollData.isDramatique) {
|
||||
totalCost = Math.ceil((totalCost + 1) / 2)
|
||||
}
|
||||
actor.subPointsAme(rollData.runemode, totalCost)
|
||||
|
||||
// Échec dramatique : dé du Chaos
|
||||
if (rollData.isDramatique) {
|
||||
const chaosRoll = await new Roll("1d20").evaluate()
|
||||
await this.showDiceSoNice(chaosRoll, game.settings.get("core", "rollMode"))
|
||||
const cr = chaosRoll.terms[0].results[0].result
|
||||
rollData.chaosDieResult = cr
|
||||
const claValue = rollData.attr?.value ?? 0
|
||||
if (cr === 1 || cr === 11) {
|
||||
rollData.chaosEffet = "desastre"
|
||||
rollData.chaosEffetTexte = `Désastre extraordinaire ! Les Runes se déclenchent à des kilomètres de là sur des cibles inconnues. La Loi se manifeste : le sorcier ne peut plus utiliser l'Œil pendant ${claValue} semaine${claValue > 1 ? "s" : ""}.`
|
||||
} else if (cr % 2 === 1) {
|
||||
rollData.chaosEffet = "echec_absolu"
|
||||
rollData.chaosEffetTexte = "Échec absolu. Le MJ décide si les Runes se manifestent sur des cibles autres, dans des proportions désavantageuses ou en un lieu très lointain."
|
||||
} else {
|
||||
rollData.chaosEffet = "rien"
|
||||
rollData.chaosEffetTexte = "Rien de particulier ne se produit en plus de la perte des points d'Âme."
|
||||
}
|
||||
}
|
||||
|
||||
// Succès : créer un runeeffect par rune
|
||||
if (rollData.isSuccess) {
|
||||
const effetMode = isInscrire ? "inscrite" : "prononcee"
|
||||
const items = rollData.sortilegeRunes.map(r => ({
|
||||
name: r.name,
|
||||
type: "runeeffect",
|
||||
img: r.img || "systems/fvtt-mournblade/assets/icons/rune.webp",
|
||||
system: {
|
||||
rune: r.name,
|
||||
mode: effetMode,
|
||||
duree: r.dureeLabel,
|
||||
pointame: r.pts
|
||||
}
|
||||
}))
|
||||
await actor.createEmbeddedDocuments("Item", items)
|
||||
}
|
||||
|
||||
rollData.runeame = rollData.sortilegeRunes.reduce((s, r) => s + r.pts, 0)
|
||||
|
||||
this.createChatWithRollMode(rollData.alias, {
|
||||
content: await foundry.applications.handlebars.renderTemplate(
|
||||
`systems/fvtt-mournblade/templates/chat-sortilege-result.hbs`, rollData)
|
||||
}, rollData)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static async rollDegatsFromAttaque(rollData) {
|
||||
let maximize = false
|
||||
@@ -517,7 +725,7 @@ export class MournbladeUtility {
|
||||
}
|
||||
|
||||
this.createChatWithRollMode(rollData.alias, {
|
||||
content: await renderTemplate(`systems/fvtt-mournblade/templates/chat-degats-result.html`, rollData)
|
||||
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-mournblade/templates/chat-degats-result-v2.hbs`, rollData)
|
||||
}, rollData)
|
||||
|
||||
}
|
||||
@@ -568,17 +776,17 @@ export class MournbladeUtility {
|
||||
|
||||
rollData.finalResult += rollData.bonusRoll.total
|
||||
|
||||
this.computeResult(rollData)
|
||||
this.computeQualityResult(rollData)
|
||||
|
||||
this.createChatWithRollMode(rollData.alias, {
|
||||
content: await renderTemplate(`systems/fvtt-mournblade/templates/chat-generic-result.html`, rollData)
|
||||
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-mournblade/templates/chat-generic-result-v2.hbs`, rollData)
|
||||
}, rollData)
|
||||
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static getUsers(filter) {
|
||||
return game.users.filter(filter).map(user => user.data._id);
|
||||
return game.users.filter(filter).map(user => user._id);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
@@ -602,7 +810,7 @@ export class MournbladeUtility {
|
||||
chatGM.whisper = this.getUsers(user => user.isGM);
|
||||
chatGM.content = "Blinde message of " + game.user.name + "<br>" + chatOptions.content;
|
||||
console.log("blindMessageToGM", chatGM);
|
||||
game.socket.emit("system.fvtt-weapons-of-the-gods", { msg: "msg_gm_chat_message", data: chatGM });
|
||||
game.socket.emit("system.fvtt-mournblade", { msg: "msg_gm_chat_message", data: chatGM });
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
@@ -676,7 +884,8 @@ export class MournbladeUtility {
|
||||
let target = MournbladeUtility.getTarget()
|
||||
if (target) {
|
||||
rollData.defenderTokenId = target.id
|
||||
let defender = game.canvas.tokens.get(rollData.defenderTokenId).actor
|
||||
let defender = game.canvas?.tokens?.get(rollData.defenderTokenId)?.actor
|
||||
if (!defender) return
|
||||
rollData.defenderCombatValues = defender.getCombatValues()
|
||||
rollData.defender = defender.toObject() // Simpler
|
||||
rollData.defenderDefense = defender.getBestDefenseValue()
|
||||
@@ -766,7 +975,7 @@ export class MournbladeUtility {
|
||||
let rollData = message.getFlag("world", "mournblade-roll")
|
||||
let actor = MournbladeUtility.getActorFromRollData(rollData)
|
||||
if (rollData.competence) {
|
||||
let nbPred = rollData.competence.data.predilections.filter(pred => !pred.used).length
|
||||
let nbPred = rollData.competence.system.predilections.filter(pred => !pred.used).length
|
||||
return (!rollData.isReroll && rollData.competence && nbPred > 0)
|
||||
}
|
||||
return false
|
||||
@@ -776,7 +985,7 @@ export class MournbladeUtility {
|
||||
let rollData = message.getFlag("world", "mournblade-roll")
|
||||
let actor = MournbladeUtility.getActorFromRollData(rollData)
|
||||
if (rollData.competence) {
|
||||
return rollData.competence.data.doublebonus
|
||||
return rollData.competence.system.doublebonus
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -841,7 +1050,7 @@ export class MournbladeUtility {
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static async confirmDelete(actorSheet, li) {
|
||||
let itemId = li.data("item-id");
|
||||
let itemId = li.dataset?.itemId || li.data("item-id");
|
||||
let msgTxt = "<p>Voulez vous supprimer cet item ?";
|
||||
let buttons = {
|
||||
delete: {
|
||||
@@ -849,7 +1058,7 @@ export class MournbladeUtility {
|
||||
label: "Oui !",
|
||||
callback: () => {
|
||||
actorSheet.actor.deleteEmbeddedDocuments("Item", [itemId]);
|
||||
li.slideUp(200, () => actorSheet.render(false));
|
||||
actorSheet.render(false);
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
@@ -867,4 +1076,443 @@ export class MournbladeUtility {
|
||||
d.render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/**
|
||||
* Roll for potion preparation (blind mode — GM only sees result)
|
||||
* @param {object} rollData
|
||||
*/
|
||||
static async rollPotion(rollData) {
|
||||
if (!rollData.runeId) {
|
||||
ui.notifications.warn("Aucune Rune sélectionnée pour la préparation de la potion.")
|
||||
return
|
||||
}
|
||||
|
||||
const actor = game.actors.get(rollData.actorId)
|
||||
|
||||
const pa = rollData.pointsAme ?? 1
|
||||
const seuil = rollData.runeSeuil ?? 0
|
||||
const difficulte = seuil + pa
|
||||
const modificateur = rollData.modificateur ?? 0
|
||||
rollData.difficulte = difficulte
|
||||
|
||||
const compNiveau = rollData.competence?.system?.niveau ?? 0
|
||||
const compMod = compNiveau === 0 ? -3 : 0
|
||||
|
||||
rollData.diceFormula = `${rollData.mainDice ?? "1d10"}+${rollData.attr.value}+${compNiveau}+${modificateur}+${compMod}+${rollData.malusSante}+${rollData.malusAme}`
|
||||
|
||||
const myRoll = await new Roll(rollData.diceFormula).evaluate()
|
||||
await this.showDiceSoNice(myRoll, "blindroll")
|
||||
rollData.roll = foundry.utils.duplicate(myRoll)
|
||||
rollData.diceResult = myRoll.terms[0].results[0].result
|
||||
rollData.finalResult = myRoll.total
|
||||
this.computeResult(rollData)
|
||||
|
||||
// Determine potion status
|
||||
let potionStatut
|
||||
let virulence = 0
|
||||
let ameDeduct = pa
|
||||
let potionCreated = false
|
||||
|
||||
if (rollData.isHeroique) {
|
||||
potionStatut = "heroique"
|
||||
} else if (rollData.isSuccess) {
|
||||
potionStatut = "efficace"
|
||||
} else if (rollData.isDramatique) {
|
||||
potionStatut = "inconnue"
|
||||
virulence = pa * 3
|
||||
} else {
|
||||
potionStatut = "inefficace"
|
||||
ameDeduct = Math.ceil(pa / 2)
|
||||
}
|
||||
|
||||
rollData.virulence = virulence
|
||||
actor.subPointsAme("prononcer", ameDeduct)
|
||||
|
||||
// Calculate durations and prep time
|
||||
const forme = rollData.forme ?? "liquide"
|
||||
const isSolide = ["onguent", "cachets", "pilules"].includes(forme)
|
||||
const dureeHeures = pa
|
||||
const conservationMois = isSolide ? pa * 6 : pa
|
||||
const tempsPrep = Math.max(1, Math.ceil(pa / 3))
|
||||
rollData.dureePotion = dureeHeures === 1 ? "1 heure" : `${dureeHeures} heures`
|
||||
rollData.conservationPotion = `${conservationMois} mois`
|
||||
rollData.tempsPreparation = tempsPrep === 1 ? "1 heure" : `${tempsPrep} heures`
|
||||
|
||||
const formeLabels = { liquide: "Liquide", onguent: "Onguent", cachets: "Cachets", pilules: "Pilules" }
|
||||
rollData.formeLabel = formeLabels[forme] ?? forme
|
||||
|
||||
if (potionStatut !== "inefficace") {
|
||||
const potionItem = {
|
||||
name: `Potion de ${rollData.runeName}`,
|
||||
type: "potion",
|
||||
img: rollData.runeImg || "systems/fvtt-mournblade/assets/icons/potion.webp",
|
||||
system: {
|
||||
rune: rollData.runeName,
|
||||
runeImg: rollData.runeImg ?? "",
|
||||
runeSeuil: seuil,
|
||||
pointsAme: pa,
|
||||
forme: forme,
|
||||
statut: potionStatut,
|
||||
virulence: virulence,
|
||||
duree: rollData.dureePotion,
|
||||
conservation: rollData.conservationPotion,
|
||||
tempsPreparation: rollData.tempsPreparation,
|
||||
}
|
||||
}
|
||||
await actor.createEmbeddedDocuments("Item", [potionItem])
|
||||
potionCreated = true
|
||||
}
|
||||
|
||||
rollData.potionCreated = potionCreated
|
||||
rollData.isGM = game.user.isGM
|
||||
|
||||
this.createChatWithRollMode(rollData.alias, {
|
||||
content: await foundry.applications.handlebars.renderTemplate(
|
||||
`systems/fvtt-mournblade/templates/chat-potion-result.hbs`, rollData)
|
||||
}, { ...rollData, rollMode: "blindroll" })
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static async rollInvocationElementaire(rollData) {
|
||||
const actor = rollData.tokenId
|
||||
? game.canvas.tokens.get(rollData.tokenId)?.actor
|
||||
: game.actors.get(rollData.actorId)
|
||||
|
||||
if (!actor) {
|
||||
ui.notifications.error("Acteur introuvable pour l'invocation.")
|
||||
return
|
||||
}
|
||||
|
||||
const soulCost = rollData.invocationSoulCost ?? rollData.invocationSeuil ?? 15
|
||||
const bonusPacte = rollData.bonusPacte ?? 0
|
||||
const compNiveau = rollData.competence?.system?.niveau ?? 0
|
||||
const compMod = compNiveau === 0 ? -3 : 0
|
||||
const modificateur = rollData.modificateur ?? 0
|
||||
|
||||
// Validate that the actor has enough soul to invoke
|
||||
const ameDisponible = Math.max(0, actor.system.ame.currentmax - actor.system.ame.value)
|
||||
if (ameDisponible < soulCost) {
|
||||
ui.notifications.warn(`Âme insuffisante pour cette invocation (requis : ${soulCost}, disponible : ${ameDisponible}).`)
|
||||
return
|
||||
}
|
||||
|
||||
rollData.difficulte = rollData.invocationSeuil
|
||||
rollData.diceFormula = `${rollData.mainDice ?? "1d10"}+${rollData.attr.value}+${compNiveau}+${bonusPacte}+${modificateur}+${compMod}+${rollData.malusSante}+${rollData.malusAme}`
|
||||
|
||||
const myRoll = await new Roll(rollData.diceFormula).evaluate()
|
||||
await this.showDiceSoNice(myRoll, "blindroll")
|
||||
rollData.roll = foundry.utils.duplicate(myRoll)
|
||||
rollData.diceResult = myRoll.terms[0].results[0].result
|
||||
rollData.finalResult = myRoll.total
|
||||
this.computeResult(rollData)
|
||||
|
||||
let ameDeduct = soulCost
|
||||
let elementaireCreated = false
|
||||
let createdActorId = null
|
||||
let createdActorName = null
|
||||
|
||||
if (rollData.isSuccess || rollData.isHeroique) {
|
||||
// Build elemental name for compendium lookup
|
||||
const elementNames = { air: "d'Air", terre: "de Terre", feu: "de Feu", eau: "de l'Eau" }
|
||||
const tierNames = { mineur: "Mineur", median: "Médian", majeur: "Majeur" }
|
||||
const elemLabel = elementNames[rollData.invocationElement] ?? rollData.invocationElement
|
||||
const tierLabel = tierNames[rollData.invocationTier] ?? rollData.invocationTier
|
||||
const searchName = `Élémentaire ${elemLabel} ${tierLabel}`
|
||||
|
||||
// Import from compendium
|
||||
const pack = game.packs.get("fvtt-mournblade.creatures-elementaires")
|
||||
if (pack) {
|
||||
const packIndex = await pack.getIndex()
|
||||
const entry = packIndex.find(e => e.name === searchName)
|
||||
if (entry) {
|
||||
const doc = await pack.getDocument(entry._id)
|
||||
if (doc) {
|
||||
const createdActors = await Actor.createDocuments([doc.toObject()], { renderSheet: false })
|
||||
const createdActor = createdActors[0]
|
||||
if (createdActor) {
|
||||
// Set elemental soul = soulCost invested by invoker
|
||||
await createdActor.update({
|
||||
"system.ame.fullmax": soulCost,
|
||||
"system.ame.currentmax": soulCost,
|
||||
"system.ame.value": 0,
|
||||
})
|
||||
createdActorId = createdActor.id
|
||||
createdActorName = createdActor.name
|
||||
elementaireCreated = true
|
||||
|
||||
// Soul blocked only on confirmed elemental creation
|
||||
await actor.subPointsAme("prononcer", soulCost)
|
||||
|
||||
// Track invocation on personnage
|
||||
const invocations = foundry.utils.duplicate(actor.system.invocationsElementaires || [])
|
||||
invocations.push({
|
||||
element: rollData.invocationElement,
|
||||
tier: rollData.invocationTier,
|
||||
soulCost,
|
||||
actorId: createdActorId,
|
||||
actorName: createdActorName,
|
||||
})
|
||||
await actor.update({ "system.invocationsElementaires": invocations })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.notifications.warn(`Élémentaire "${searchName}" introuvable dans le compendium creatures-elementaires.`)
|
||||
}
|
||||
} else {
|
||||
ui.notifications.warn("Compendium creatures-elementaires introuvable.")
|
||||
}
|
||||
} else if (rollData.isDramatique) {
|
||||
// All soul lost
|
||||
await actor.subPointsAme("prononcer", soulCost)
|
||||
} else {
|
||||
// Simple failure: half soul lost (round up)
|
||||
ameDeduct = Math.ceil(soulCost / 2)
|
||||
await actor.subPointsAme("prononcer", ameDeduct)
|
||||
}
|
||||
|
||||
rollData.invocationSoulDeducted = rollData.isSuccess || rollData.isHeroique ? soulCost : ameDeduct
|
||||
rollData.elementaireCreated = elementaireCreated
|
||||
rollData.createdActorName = createdActorName
|
||||
rollData.bonusPacte = bonusPacte
|
||||
rollData.isGM = game.user.isGM
|
||||
const powersByTier = { mineur: 2, median: 3, majeur: 4 }
|
||||
rollData.invocationPowerCount = powersByTier[rollData.invocationTier] ?? 2
|
||||
|
||||
this.createChatWithRollMode(rollData.alias, {
|
||||
content: await foundry.applications.handlebars.renderTemplate(
|
||||
`systems/fvtt-mournblade/templates/chat-invocation-result.hbs`, rollData)
|
||||
}, { ...rollData, rollMode: "blindroll" })
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static async rollInvocationDemon(rollData) {
|
||||
const actor = rollData.tokenId
|
||||
? game.canvas.tokens.get(rollData.tokenId)?.actor
|
||||
: game.actors.get(rollData.actorId)
|
||||
|
||||
if (!actor) {
|
||||
ui.notifications.error("Acteur introuvable pour l'invocation démoniaque.")
|
||||
return
|
||||
}
|
||||
|
||||
const soulCost = rollData.invocationSoulCost ?? rollData.invocationSeuil ?? 20
|
||||
const compNiveau = rollData.competence?.system?.niveau ?? 0
|
||||
const compMod = compNiveau === 0 ? -3 : 0
|
||||
const modificateur = rollData.modificateur ?? 0
|
||||
|
||||
// Validate soul
|
||||
const ameDisponible = Math.max(0, actor.system.ame.currentmax - actor.system.ame.value)
|
||||
if (ameDisponible < soulCost) {
|
||||
ui.notifications.warn(`Âme insuffisante pour cette invocation (requis : ${soulCost}, disponible : ${ameDisponible}).`)
|
||||
return
|
||||
}
|
||||
|
||||
rollData.difficulte = rollData.invocationSeuil
|
||||
rollData.diceFormula = `${rollData.mainDice ?? "1d10"}+${rollData.attr.value}+${compNiveau}+${modificateur}+${compMod}+${rollData.malusSante}+${rollData.malusAme}`
|
||||
|
||||
const myRoll = await new Roll(rollData.diceFormula).evaluate()
|
||||
await this.showDiceSoNice(myRoll, "blindroll")
|
||||
rollData.roll = foundry.utils.duplicate(myRoll)
|
||||
rollData.diceResult = myRoll.terms[0].results[0].result
|
||||
rollData.finalResult = myRoll.total
|
||||
this.computeResult(rollData)
|
||||
|
||||
// Soul cost handling
|
||||
let ameDeduct = soulCost
|
||||
let d20Result = null
|
||||
let isDemonAttaque = false
|
||||
let isDisastreDramatique = false
|
||||
let isTraitChaotique = false
|
||||
|
||||
if (rollData.isSuccess || rollData.isHeroique) {
|
||||
// Soul spent immediately (not blocked)
|
||||
await actor.subPointsAme("prononcer", soulCost)
|
||||
|
||||
// Track active invocation
|
||||
const invocations = foundry.utils.duplicate(actor.system.invocationsDemons || [])
|
||||
invocations.push({ demonName: "Démon invoqué", soulCost, date: Date.now() })
|
||||
await actor.update({ "system.invocationsDemons": invocations })
|
||||
} else if (rollData.isDramatique) {
|
||||
// All soul lost
|
||||
await actor.subPointsAme("prononcer", soulCost)
|
||||
|
||||
// Roll d20 for dramatic failure consequences
|
||||
const d20Roll = await new Roll("1d20").evaluate()
|
||||
await this.showDiceSoNice(d20Roll, "blindroll")
|
||||
d20Result = d20Roll.total
|
||||
if (d20Result === 1 || d20Result === 11) {
|
||||
isDisastreDramatique = true
|
||||
} else if (d20Result % 2 !== 0) {
|
||||
// Odd (not 1 or 11) → demon attacks
|
||||
isDemonAttaque = true
|
||||
} else {
|
||||
// Even → chaotic trait
|
||||
isTraitChaotique = true
|
||||
}
|
||||
} else {
|
||||
// Simple failure: half soul lost (round up)
|
||||
ameDeduct = Math.ceil(soulCost / 2)
|
||||
await actor.subPointsAme("prononcer", ameDeduct)
|
||||
}
|
||||
|
||||
rollData.invocationSoulDeducted = (rollData.isSuccess || rollData.isHeroique) ? soulCost : ameDeduct
|
||||
rollData.d20Result = d20Result
|
||||
rollData.isDemonAttaque = isDemonAttaque
|
||||
rollData.isDisastreDramatique = isDisastreDramatique
|
||||
rollData.isTraitChaotique = isTraitChaotique
|
||||
rollData.isGM = game.user.isGM
|
||||
rollData.claValue = actor.system.attributs?.cla?.value ?? 0
|
||||
|
||||
this.createChatWithRollMode(rollData.alias, {
|
||||
content: await foundry.applications.handlebars.renderTemplate(
|
||||
`systems/fvtt-mournblade/templates/chat-invocation-demon-result.hbs`, rollData)
|
||||
}, { ...rollData, rollMode: "blindroll" })
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static async rollInvocationEsprit(rollData) {
|
||||
const actor = rollData.tokenId
|
||||
? game.canvas.tokens.get(rollData.tokenId)?.actor
|
||||
: game.actors.get(rollData.actorId)
|
||||
|
||||
if (!actor) {
|
||||
ui.notifications.error("Acteur introuvable pour l'invocation d'un Esprit de la Loi.")
|
||||
return
|
||||
}
|
||||
|
||||
const soulCost = rollData.invocationSoulCost ?? 15
|
||||
const compNiveau = rollData.competence?.system?.niveau ?? 0
|
||||
const compMod = compNiveau === 0 ? -3 : 0
|
||||
const modificateur = rollData.modificateur ?? 0
|
||||
|
||||
const ameDisponible = Math.max(0, actor.system.ame.currentmax - actor.system.ame.value)
|
||||
if (ameDisponible < soulCost) {
|
||||
ui.notifications.warn(`Âme insuffisante (requis : ${soulCost}, disponible : ${ameDisponible}).`)
|
||||
return
|
||||
}
|
||||
|
||||
rollData.difficulte = soulCost
|
||||
rollData.diceFormula = `${rollData.mainDice ?? "1d10"}+${rollData.attr.value}+${compNiveau}+${modificateur}+${compMod}+${rollData.malusSante}+${rollData.malusAme}`
|
||||
|
||||
const myRoll = await new Roll(rollData.diceFormula).evaluate()
|
||||
await this.showDiceSoNice(myRoll, "roll")
|
||||
rollData.roll = foundry.utils.duplicate(myRoll)
|
||||
rollData.diceResult = myRoll.terms[0].results[0].result
|
||||
rollData.finalResult = myRoll.total
|
||||
this.computeResult(rollData)
|
||||
|
||||
let ameDeduct = soulCost
|
||||
|
||||
if (rollData.isSuccess || rollData.isHeroique) {
|
||||
await actor.subPointsAme("prononcer", soulCost)
|
||||
} else if (rollData.isDramatique) {
|
||||
// All soul lost, Réceptacle destroyed
|
||||
await actor.subPointsAme("prononcer", soulCost)
|
||||
} else {
|
||||
// Simple failure: half soul lost (round up)
|
||||
ameDeduct = Math.ceil(soulCost / 2)
|
||||
await actor.subPointsAme("prononcer", ameDeduct)
|
||||
}
|
||||
|
||||
rollData.invocationSoulDeducted = (rollData.isSuccess || rollData.isHeroique) ? soulCost : ameDeduct
|
||||
rollData.isGM = game.user.isGM
|
||||
|
||||
const typeLabels = {
|
||||
combat: "Combat", voyage: "Voyage", perception: "Perception",
|
||||
restauration: "Restauration", reparateur: "Réparateur",
|
||||
}
|
||||
const puissanceLabels = { mineur: "Mineur", median: "Médian", majeur: "Majeur" }
|
||||
rollData.automatonTypeLabel = typeLabels[rollData.automatonType] ?? rollData.automatonType
|
||||
rollData.automatonPuissanceLabel = puissanceLabels[rollData.automatonPuissance] ?? rollData.automatonPuissance
|
||||
|
||||
this.createChatWithRollMode(rollData.alias, {
|
||||
content: await foundry.applications.handlebars.renderTemplate(
|
||||
`systems/fvtt-mournblade/templates/chat-invocation-esprit-result.hbs`, rollData)
|
||||
}, { ...rollData, rollMode: "roll" })
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static async rollEnchantement({ actor, item, ptsAme, antiChaos, modificateur,
|
||||
savoirRunesComp, hautParlerComp, artisanatComp, claValeur, limiteur }) {
|
||||
|
||||
// Validate soul
|
||||
const ameDisponible = Math.max(0, actor.system.ame.currentmax - actor.system.ame.value)
|
||||
if (ameDisponible < ptsAme) {
|
||||
ui.notifications.warn(`Âme insuffisante (requis : ${ptsAme}, disponible : ${ameDisponible}).`)
|
||||
return
|
||||
}
|
||||
|
||||
const savoirNiveau = savoirRunesComp?.system?.niveau ?? 0
|
||||
const compMod = savoirNiveau === 0 ? -3 : 0
|
||||
const basePool = claValeur + savoirNiveau + compMod + (modificateur ?? 0)
|
||||
const effectivePool = limiteur !== null ? Math.min(basePool, limiteur) : basePool
|
||||
const difficulte = ptsAme
|
||||
|
||||
const formula = `1d10+${effectivePool}`
|
||||
const myRoll = await new Roll(formula).evaluate()
|
||||
await this.showDiceSoNice(myRoll, game.settings.get("core", "rollMode"))
|
||||
|
||||
const rollData = this.getBasicRollData()
|
||||
rollData.alias = actor.name
|
||||
rollData.actorImg = actor.img
|
||||
rollData.diceResult = myRoll.terms[0].results[0].result
|
||||
rollData.finalResult = myRoll.total
|
||||
rollData.difficulte = difficulte
|
||||
rollData.roll = foundry.utils.duplicate(myRoll)
|
||||
this.computeResult(rollData)
|
||||
|
||||
// Compute bonus
|
||||
let bonusBase = Math.floor(ptsAme / 5)
|
||||
let bonusFinal = bonusBase
|
||||
let ameDeduct = ptsAme
|
||||
let itemDestroyed = false
|
||||
let message = ""
|
||||
|
||||
if (rollData.isHeroique) {
|
||||
bonusFinal = bonusBase + 1
|
||||
await actor.subPointsAme("prononcer", ptsAme)
|
||||
await item.update({
|
||||
"system.enchantementLoi.actif": true,
|
||||
"system.enchantementLoi.bonus": bonusFinal,
|
||||
"system.enchantementLoi.antiChaos": antiChaos,
|
||||
})
|
||||
message = `Réussite héroïque ! L'objet est enchanté avec un bonus de +${bonusFinal}.`
|
||||
} else if (rollData.isSuccess) {
|
||||
await actor.subPointsAme("prononcer", ptsAme)
|
||||
await item.update({
|
||||
"system.enchantementLoi.actif": true,
|
||||
"system.enchantementLoi.bonus": bonusFinal,
|
||||
"system.enchantementLoi.antiChaos": antiChaos,
|
||||
})
|
||||
message = `Succès ! L'objet est enchanté avec un bonus de +${bonusFinal}.`
|
||||
} else if (rollData.isDramatique) {
|
||||
ameDeduct = ptsAme
|
||||
await actor.subPointsAme("prononcer", ameDeduct)
|
||||
await item.delete()
|
||||
itemDestroyed = true
|
||||
message = `Échec dramatique ! Tous les points d'Âme sont perdus et l'objet est détruit !`
|
||||
} else {
|
||||
// Failure: half soul lost
|
||||
ameDeduct = Math.ceil(ptsAme / 2)
|
||||
await actor.subPointsAme("prononcer", ameDeduct)
|
||||
message = `Échec ! ${ameDeduct} points d'Âme perdus. L'objet n'est pas enchanté.`
|
||||
}
|
||||
|
||||
rollData.itemName = item.name
|
||||
rollData.itemImg = item.img
|
||||
rollData.ptsAme = ptsAme
|
||||
rollData.antiChaos = antiChaos
|
||||
rollData.bonusFinal = bonusFinal
|
||||
rollData.ameDeduct = ameDeduct
|
||||
rollData.itemDestroyed = itemDestroyed
|
||||
rollData.enchantMessage = message
|
||||
rollData.effectivePool = effectivePool
|
||||
rollData.savoirNiveau = savoirNiveau
|
||||
|
||||
this.createChatWithRollMode(actor.name, {
|
||||
content: await foundry.applications.handlebars.renderTemplate(
|
||||
`systems/fvtt-mournblade/templates/chat-enchantement-result.hbs`, rollData)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "fvtt-mournblade",
|
||||
"version": "1.0.0",
|
||||
"description": "Mournblade RPG for FoundryVTT - French",
|
||||
"scripts": {
|
||||
"build": "gulp build",
|
||||
"watch": "gulp watch"
|
||||
},
|
||||
"author": "Uberwald/LeRatierBretonnien",
|
||||
"license": "SEE LICENSE IN LICENCE.txt",
|
||||
"devDependencies": {
|
||||
"@foundryvtt/foundryvtt-cli": "^3.0.3",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-less": "^5.0.0",
|
||||
"gulp-sourcemaps": "^3.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
MANIFEST-000221
|
||||
MANIFEST-000404
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
2025/05/01-19:59:13.404410 7fad8f7fe6c0 Recovering log #219
|
||||
2025/05/01-19:59:13.404447 7fad8f7fe6c0 Recovering log #231
|
||||
2025/05/01-19:59:13.456319 7fad8f7fe6c0 Delete type=0 #231
|
||||
2025/05/01-19:59:13.456390 7fad8f7fe6c0 Delete type=0 #219
|
||||
2025/05/01-19:59:13.456430 7fad8f7fe6c0 Delete type=3 #217
|
||||
2025/05/01-20:40:06.630698 7fad8effd6c0 Level-0 table #234: started
|
||||
2025/05/01-20:40:06.634192 7fad8effd6c0 Level-0 table #234: 14422 bytes OK
|
||||
2025/05/01-20:40:06.640500 7fad8effd6c0 Delete type=0 #232
|
||||
2025/05/01-20:40:06.664516 7fad8effd6c0 Manual compaction at level-0 from '!items!0swiE8k5zfUIqmXu' @ 72057594037927935 : 1 .. '!items!wv5EiePmPTpqFutt' @ 0 : 0; will stop at (end)
|
||||
2025/05/01-20:40:06.675130 7fad8effd6c0 Manual compaction at level-1 from '!items!0swiE8k5zfUIqmXu' @ 72057594037927935 : 1 .. '!items!wv5EiePmPTpqFutt' @ 0 : 0; will stop at '!items!wv5EiePmPTpqFutt' @ 239 : 1
|
||||
2025/05/01-20:40:06.675140 7fad8effd6c0 Compacting 1@1 + 1@2 files
|
||||
2025/05/01-20:40:06.678758 7fad8effd6c0 Generated table #235@1: 48 keys, 14422 bytes
|
||||
2025/05/01-20:40:06.678804 7fad8effd6c0 Compacted 1@1 + 1@2 files => 14422 bytes
|
||||
2025/05/01-20:40:06.684949 7fad8effd6c0 compacted to: files[ 0 0 1 0 0 0 0 ]
|
||||
2025/05/01-20:40:06.685040 7fad8effd6c0 Delete type=2 #212
|
||||
2025/05/01-20:40:06.685158 7fad8effd6c0 Delete type=2 #234
|
||||
2025/05/01-20:40:06.711796 7fad8effd6c0 Manual compaction at level-1 from '!items!wv5EiePmPTpqFutt' @ 239 : 1 .. '!items!wv5EiePmPTpqFutt' @ 0 : 0; will stop at (end)
|
||||
2026/05/02-23:45:02.647102 7fd747fff6c0 Recovering log #402
|
||||
2026/05/02-23:45:02.656665 7fd747fff6c0 Delete type=3 #400
|
||||
2026/05/02-23:45:02.656732 7fd747fff6c0 Delete type=0 #402
|
||||
2026/05/02-23:46:06.817245 7fd7477fe6c0 Level-0 table #407: started
|
||||
2026/05/02-23:46:06.817291 7fd7477fe6c0 Level-0 table #407: 0 bytes OK
|
||||
2026/05/02-23:46:06.852878 7fd7477fe6c0 Delete type=0 #405
|
||||
2026/05/02-23:46:06.933278 7fd7477fe6c0 Manual compaction at level-0 from '!items!0swiE8k5zfUIqmXu' @ 72057594037927935 : 1 .. '!items!wv5EiePmPTpqFutt' @ 0 : 0; will stop at (end)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
2024/09/10-07:14:44.885743 7f7ed4e006c0 Recovering log #215
|
||||
2024/09/10-07:14:44.956966 7f7ed4e006c0 Delete type=3 #213
|
||||
2024/09/10-07:14:44.957144 7f7ed4e006c0 Delete type=0 #215
|
||||
2024/09/10-07:20:33.463210 7f7ecd6006c0 Level-0 table #220: started
|
||||
2024/09/10-07:20:33.463253 7f7ecd6006c0 Level-0 table #220: 0 bytes OK
|
||||
2024/09/10-07:20:33.501220 7f7ecd6006c0 Delete type=0 #218
|
||||
2024/09/10-07:20:33.538862 7f7ecd6006c0 Manual compaction at level-0 from '!items!0swiE8k5zfUIqmXu' @ 72057594037927935 : 1 .. '!items!wv5EiePmPTpqFutt' @ 0 : 0; will stop at (end)
|
||||
2024/09/10-07:20:33.538937 7f7ecd6006c0 Manual compaction at level-1 from '!items!0swiE8k5zfUIqmXu' @ 72057594037927935 : 1 .. '!items!wv5EiePmPTpqFutt' @ 0 : 0; will stop at (end)
|
||||
2026/05/02-23:32:07.584562 7fd7557ee6c0 Recovering log #398
|
||||
2026/05/02-23:32:07.681342 7fd7557ee6c0 Delete type=3 #396
|
||||
2026/05/02-23:32:07.681454 7fd7557ee6c0 Delete type=0 #398
|
||||
2026/05/02-23:32:54.252126 7fd7477fe6c0 Level-0 table #403: started
|
||||
2026/05/02-23:32:54.252168 7fd7477fe6c0 Level-0 table #403: 0 bytes OK
|
||||
2026/05/02-23:32:54.259560 7fd7477fe6c0 Delete type=0 #401
|
||||
2026/05/02-23:32:54.270552 7fd7477fe6c0 Manual compaction at level-0 from '!items!0swiE8k5zfUIqmXu' @ 72057594037927935 : 1 .. '!items!wv5EiePmPTpqFutt' @ 0 : 0; will stop at (end)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
MANIFEST-000023
|
||||