Compare commits

...

10 Commits

Author SHA1 Message Date
uberwald 93f07adcee Ajout démons
Release Creation / build (release) Successful in 1m38s
2026-05-03 16:14:42 +02:00
uberwald eac8d93670 Ajout démons 2026-05-03 16:14:28 +02:00
uberwald 1cc6f92f15 Ajout démons 2026-05-03 16:03:45 +02:00
uberwald 0df4a5a9fb Esprit de la Loi + Automaton 2026-05-02 23:16:10 +02:00
uberwald d6b5891519 Potions et élémentaires 2026-05-02 08:26:28 +02:00
uberwald a234ba5d14 COrrection sur predilections
Release Creation / build (release) Successful in 54s
2026-04-30 23:34:16 +02:00
uberwald 463e9ebb19 Correction sur les runes actives et update compendiums
Release Creation / build (release) Successful in 51s
2026-04-09 23:48:01 +02:00
uberwald d8cfdfb96d Correction pour l'activation des runes, qui ne marchai plus 2026-04-08 23:53:32 +02:00
uberwald 1e63db8c39 Foundryv14 migration 2026-04-01 22:40:03 +02:00
uberwald b97f8688e4 Foundryv14 migration
Release Creation / build (release) Successful in 46s
2026-04-01 22:39:25 +02:00
468 changed files with 20912 additions and 599 deletions
+50 -1
View File
@@ -30,6 +30,55 @@ jobs:
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
@@ -60,4 +109,4 @@ jobs:
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: '13'
compatibility-verified: '14'
+1
View File
@@ -1,3 +1,4 @@
.history/
node_modules
.github/
regles/
+1
View File
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

+145 -26
View File
@@ -171,34 +171,153 @@
"MNBL.weapontype": "Weapon Type",
"MNBL.weight": "Weight",
"MNBL.total": "Total",
"Présence": "Presence",
"Puissance": "Might",
"Trempe": "Mettle",
"TYPES": {
"Actor": {
"creature": "Creature",
"personnage": "Character"
},
"Item": {
"arme": "Weapon",
"bouclier": "Shield",
"capacite": "Ability",
"competence": "Skill",
"don": "Gifts",
"equipement": "Equipment",
"heritage": "Background",
"metier": "Profession",
"modifier": "Modifier",
"monnaie": "Currency",
"origine": "Origin",
"pacte": "Pacts",
"protection": "Protections",
"rune": "Rune",
"runeeffect": "Rune Effect",
"tendance": "Signs of Chaos",
"traitchaotique": "Background",
"traitespece": "Species Trait"
}
}
"Actor": {
"creature": "Creature",
"personnage": "Character"
},
"Item": {
"arme": "Weapon",
"bouclier": "Shield",
"capacite": "Ability",
"competence": "Skill",
"don": "Gifts",
"equipement": "Equipment",
"heritage": "Background",
"metier": "Profession",
"modifier": "Modifier",
"monnaie": "Currency",
"origine": "Origin",
"pacte": "Pacts",
"protection": "Protections",
"rune": "Rune",
"runeeffect": "Rune Effect",
"tendance": "Signs of Chaos",
"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)"
}
+132 -24
View File
@@ -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,14 +90,14 @@
"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é",
"MNBL.damagebonus": "B. Dégats",
"MNBL.speed": "Vitesse",
"MNBL.speed": "Vitesse",
"MNBL.defense": "Défense",
"MNBL.totalprotection": "Protection Totale",
"MNBL.totalprotection": "Protection Totale",
"MNBL.modifier": "Modificateurs",
"MNBL.type": "Type",
"MNBL.value": "Valeur",
@@ -133,10 +126,10 @@
"MNBL.protections": "Protections",
"MNBL.equipments": "Equipements",
"MNBL.equipment": "Equipement",
"MNBL.origin": "Origine",
"MNBL.origin": "Origine",
"MNBL.legacy": "Héritage",
"MNBL.profession": "Métier",
"MNBL.genre": "Genre",
"MNBL.genre": "Genre",
"MNBL.size": "Taille",
"MNBL.hair": "Cheveux",
"MNBL.eyes": "Yeux",
@@ -145,7 +138,6 @@
"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,13 +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",
@@ -182,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é",
@@ -203,15 +193,14 @@
"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",
@@ -220,6 +209,125 @@
"MNBL.details": "Détails",
"MNBL.sacrifice": "Sacrifice",
"MNBL.identity": "Identité",
"MNBL.gmtools": "Outils MJ"
"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)"
}
+78 -2
View File
@@ -343,8 +343,8 @@
}
.item-controls-fixed {
min-width: 3.2rem;
max-width: 3.2rem;
min-width: 5rem;
max-width: 5rem;
}
// Couleurs pour les labels et textes dans les onglets
@@ -872,3 +872,79 @@
}
}
}
/* ==================== 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);
}
}
}
+202 -21
View File
@@ -117,27 +117,29 @@
}
}
.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;
}
i {
font-size: 0.9rem;
}
.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;
&:hover {
background: linear-gradient(to bottom, #800000 5%, #3e0101 100%);
color: #ffffff;
box-shadow: 0 0 8px rgba(128, 0, 0, 0.6);
}
i {
font-size: 0.9rem;
}
&:active {
position: relative;
top: 1px;
}
&: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;
}
}
@@ -229,8 +231,21 @@
&.flexrow {
display: flex;
flex-direction: row;
align-items: center;
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;
}
}
}
@@ -275,8 +290,9 @@
}
.item-field-label-long3 {
flex: 1;
min-width: 350px;
max-width: 350px;
min-width: 250px;
white-space: normal;
line-height: 1.3;
}
.numeric-input {
@@ -297,3 +313,168 @@
}
}
}
/* ============================================= */
/* 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;
}
}
+732 -78
View File
@@ -194,36 +194,24 @@
.flex-group-center,
.flex-group-left,
.flex-group-right {
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
text-align: center;
padding: 5px;
}
.flex-group-left {
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
text-align: left;
}
.flex-group-right {
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
text-align: right;
}
.flex-center {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
text-align: center;
}
@@ -233,40 +221,22 @@
}
.flex-between {
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
}
.flex-shrink {
flex: 'flex-shrink';
}
/* Styles limited to foundryvtt-vadentis sheets */
.fvtt-mournblade .sheet-header {
-webkit-box-flex: 0;
-ms-flex: 0 0 210px;
flex: 0 0 210px;
overflow: hidden;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
margin-bottom: 10px;
}
.fvtt-mournblade .sheet-header .profile-img {
-webkit-box-flex: 0;
-ms-flex: 0 0 128px;
flex: 0 0 128px;
height: 128px;
width: 128px;
@@ -306,8 +276,6 @@
}
.fvtt-mournblade .sheet-header .header-fields {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
}
@@ -332,8 +300,6 @@
}
.fvtt-mournblade .sheet-tabs {
-webkit-box-flex: 0;
-ms-flex: 0;
flex: 0;
}
@@ -421,8 +387,6 @@
}
.fvtt-mournblade .items-list .item .item-image {
-webkit-box-flex: 0;
-ms-flex: 0 0 24px;
flex: 0 0 24px;
margin-right: 5px;
}
@@ -436,8 +400,6 @@
}
.fvtt-mournblade .items-list .item-controls {
-webkit-box-flex: 0;
-ms-flex: 0 0 86px;
flex: 0 0 86px;
text-align: right;
}
@@ -968,6 +930,165 @@
height: 64px;
}
// =====================================================
// Item Card (post-item, chat-display-description)
// =====================================================
.mournblade-item-card {
font-family: CentaurMT, serif;
color: #2a1a0a;
background: linear-gradient(180deg, #fdf6e3 0%, #f7ead0 100%);
border: 1px solid #8b4513;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
.item-card-header {
display: flex;
align-items: center;
gap: 10px;
background: linear-gradient(135deg, #4a0404 0%, #8b0000 50%, #6b2020 100%);
padding: 10px 12px;
border-bottom: 2px solid #c8860a;
.item-card-img {
width: 56px;
height: 56px;
border-radius: 4px;
border: 2px solid #c8860a;
object-fit: cover;
flex-shrink: 0;
background: rgba(0,0,0,0.3);
}
.item-card-title {
flex: 1;
min-width: 0;
.item-card-name {
margin: 0 0 4px 0;
padding: 0;
font-family: Charlemagne, CentaurMT, serif;
font-size: 1.05rem;
font-weight: bold;
color: #f5deb3;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.6);
white-space: normal;
line-height: 1.2;
border: none;
}
.item-card-type-badge {
display: inline-block;
background: rgba(200, 134, 10, 0.3);
border: 1px solid #c8860a;
border-radius: 10px;
padding: 1px 8px;
font-size: 0.72rem;
color: #f5deb3;
letter-spacing: 0.05em;
text-transform: uppercase;
i { margin-right: 3px; }
}
}
}
.item-card-stats {
padding: 8px 12px;
border-bottom: 1px solid rgba(139, 69, 19, 0.2);
display: flex;
flex-direction: column;
gap: 3px;
.stat-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
font-size: 0.85rem;
padding: 2px 0;
border-bottom: 1px dotted rgba(139, 69, 19, 0.12);
&:last-child { border-bottom: none; }
.stat-label {
color: #6b3a1f;
font-weight: bold;
flex-shrink: 0;
i { margin-right: 4px; color: #8b4513; }
}
.stat-value {
color: #2a1a0a;
text-align: right;
font-weight: normal;
&.stat-highlight {
font-weight: bold;
color: #4a0404;
font-size: 0.95rem;
}
}
&.stat-flag {
justify-content: flex-start;
color: #6b3a1f;
font-style: italic;
font-size: 0.8rem;
border-bottom: none;
padding: 1px 0;
i { margin-right: 5px; color: #8b0000; }
}
}
}
.item-card-divider {
display: flex;
align-items: center;
padding: 6px 12px 0;
gap: 8px;
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #8b4513;
font-weight: bold;
&::before, &::after {
content: '';
flex: 1;
height: 1px;
background: linear-gradient(90deg, transparent, #c8860a, transparent);
}
}
.item-card-description {
padding: 8px 12px 10px;
font-size: 0.88rem;
line-height: 1.5;
color: #2a1a0a;
p { margin: 0 0 6px 0; &:last-child { margin-bottom: 0; } }
em, i { color: #5a2a0a; }
strong, b { color: #2a1a0a; }
}
.item-card-sacrifice {
background: rgba(100, 0, 0, 0.05);
border-top: 1px solid rgba(139, 0, 0, 0.2);
font-style: italic;
color: #5a0000;
}
&.mournblade-item-card--compact {
.item-card-header {
padding: 8px 10px;
.item-card-img { width: 44px; height: 44px; }
.item-card-name { font-size: 0.95rem; }
}
.item-card-description { padding: 6px 10px 8px; }
}
}
.roll-dialog-header {
height: 52px;
}
@@ -1009,7 +1130,6 @@
height: 15%;
font-size: 15px;
padding: 10px;
padding-bottom: 20px;
padding-top: .2rem;
padding-bottom: .2rem;
}
@@ -1020,7 +1140,6 @@
}
.div-river {
align-content: center;
margin-left: 8px;
align-content: space-around;
justify-content: space-around;
@@ -1070,22 +1189,6 @@
}
/*#sidebar #sidebar-tabs i.fa-comments:before, #sidebar #sidebar-tabs i.fa-fist-raised:before, #sidebar #sidebar-tabs i.fa-users:before, #sidebar #sidebar-tabs i.fa-map:before, #sidebar #sidebar-tabs i.fa-suitcase:before, #sidebar #sidebar-tabs i.fa-book-open:before, #sidebar #sidebar-tabs i.fa-th-list:before, #sidebar #sidebar-tabs i.fa-music:before, #sidebar #sidebar-tabs i.fa-atlas:before, #sidebar #sidebar-tabs i.fa-cogs:before {content: "";}
#sidebar #sidebar-tabs i.fa-comments {background: url("img/ui/icon_sidebar_chat.svg") no-repeat;}
#sidebar #sidebar-tabs i.fa-fist-raised {background: url("img/ui/icon_sidebar_fight.svg") no-repeat;}
#sidebar #sidebar-tabs i.fa-users {background: url("img/ui/icon_sidebar_actor.svg") no-repeat;}
#sidebar #sidebar-tabs i.fa-map {background: url("img/ui/icon_sidebar_scene.svg") no-repeat;}
#sidebar #sidebar-tabs i.fa-suitcase {background: url("img/ui/icon_sidebar_item.svg") no-repeat;}
#sidebar #sidebar-tabs i.fa-book-open {background: url("img/ui/icon_sidebar_journal.svg") no-repeat;}
#sidebar #sidebar-tabs i.fa-th-list {background: url("img/ui/icon_sidebar_rolltable.svg") no-repeat;}
#sidebar #sidebar-tabs i.fa-music {background: url("img/ui/icon_sidebar_music.svg") no-repeat;}
#sidebar #sidebar-tabs i.fa-atlas {background: url("img/ui/icon_sidebar_compendium.svg") no-repeat;}
#sidebar #sidebar-tabs i.fa-cogs {background: url("img/ui/icon_sidebar_settings.svg") no-repeat;}
#combat #combat-controls {
box-shadow: inset 0 0 2rem rgba(0,0,0,0.5);
}
*/
/*--------------------------------------------------------------------------*/
/* Control, Tool, hotbar & navigation */
@@ -1239,7 +1342,6 @@
.river-button {
box-shadow: inset 0px 1px 0px 0px #a6827e;
background: linear-gradient(to bottom, #21374afc 5%, #152833ab 100%);
background-color: #7d5d3b00;
border-radius: 3px;
border: 2px ridge #846109;
display: inline-block;
@@ -1256,7 +1358,6 @@
.chat-card-button {
box-shadow: inset 0px 1px 0px 0px #a6827e;
background: linear-gradient(to bottom, #21374afc 5%, #152833ab 100%);
background-color: #7d5d3b00;
border-radius: 3px;
border: 2px ridge #846109;
display: inline-block;
@@ -1272,14 +1373,15 @@
.chat-card-button-degats {
background: linear-gradient(to bottom, #e2c256fc 5%, #b85b04ab 100%);
background-color: #7d5d3b00;
}
.river-button:hover,
.plus-minus-button:hover,
.chat-card-button:hover {
background: linear-gradient(to bottom, #800000 5%, #3e0101 100%);
background-color: red;
}
.plus-minus-button:active,
.chat-card-button:active {
position: relative;
top: 1px;
@@ -1288,7 +1390,6 @@
.button-sheet-roll {
box-shadow: inset 0px 1px 0px 0px #a6827e;
background: linear-gradient(to bottom, #21374afc 5%, #152833ab 60%);
background-color: #7d5d3b00;
border-radius: 4px;
border: 1px ridge #846109;
display: inline-block;
@@ -1307,7 +1408,6 @@
.button-sheet-roll-offline {
box-shadow: inset 0px 1px 0px 0px #a6827e;
background: linear-gradient(to bottom, #313131fc 5%, #dee2e4ab 60%);
background-color: #7d5d3b00;
border-radius: 4px;
border: 1px ridge #846109;
display: inline-block;
@@ -1329,7 +1429,6 @@
.button-sheet-roll:hover {
background: linear-gradient(to bottom, #800000 5%, #3e0101 100%);
background-color: red;
}
.button-sheet-roll:active {
@@ -1340,7 +1439,6 @@
.plus-minus-button {
box-shadow: inset 0px 1px 0px 0px #a6827e;
background: linear-gradient(to bottom, #21374afc 5%, #152833ab 100%);
background-color: #7d5d3b00;
border-radius: 3px;
border: 2px ridge #846109;
display: inline-block;
@@ -1354,19 +1452,6 @@
max-width: 1.2rem;
}
.river-button:hover,
.plus-minus-button:hover,
.chat-card-button:hover {
background: linear-gradient(to bottom, #800000 5%, #3e0101 100%);
background-color: red;
}
.plus-minus-button:active,
.chat-card-button:active {
position: relative;
top: 1px;
}
.plus-minus {
font-size: 0.9rem;
font-weight: bold;
@@ -1396,7 +1481,6 @@
.drop-spec1,
.drop-spec2 {
background: linear-gradient(to bottom, #6c95b9fc 5%, #105177ab 100%);
background-color: #7d5d3b00;
border-radius: 3px;
border: 2px ridge #846109;
}
@@ -2259,6 +2343,25 @@
&:last-child {
margin-bottom: 0;
}
&.next-steps {
background: rgba(200, 220, 255, 0.5);
border-left-color: #2255aa;
ol {
margin: 6px 0 0 0;
padding-left: 18px;
li {
margin-bottom: 4px;
font-size: 0.82rem;
}
}
i {
color: #2255aa;
}
}
}
.damage-buttons {
@@ -2287,6 +2390,40 @@
}
}
.result-chaos {
padding: 8px;
background: rgba(40, 0, 60, 0.85);
border-top: 2px solid #9400d3;
border-left: 4px solid #ff00ff;
color: #f0d0ff;
font-size: 0.85rem;
.chaos-header {
font-weight: bold;
font-size: 1rem;
margin-bottom: 6px;
color: #ff88ff;
i { margin-right: 6px; }
}
.chaos-effect {
line-height: 1.4;
i { margin-right: 6px; }
&.desastre {
color: #ff4444;
font-weight: bold;
}
&.echec_absolu {
color: #ffaa44;
}
&.rien {
color: #aaaacc;
font-style: italic;
}
}
}
.predilection-section {
padding: 8px;
background: rgba(220, 200, 255, 0.4);
@@ -2428,6 +2565,11 @@
text-decoration: none;
transition: all 0.2s;
font-size: 0.9rem;
background: none;
border: none;
padding: 0;
cursor: pointer;
font-family: inherit;
i {
margin-right: 4px;
@@ -2473,4 +2615,516 @@
}
}
// =====================================================
// Sortilège dialog
// =====================================================
.sortilege-dialog,
.potion-dialog,
.invocation-dialog {
font-family: CentaurMT, serif;
color: #2a1a0a;
padding: 10px;
background: url("../assets/ui/pc_sheet_bg.webp") repeat left top;
// Texte de base sombre sur fond parchemin
label, input, span, p {
color: #2a1a0a;
}
th, td {
color: #2a1a0a;
}
.sortilege-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
padding: 6px 10px;
background: linear-gradient(135deg, #4a0404 0%, #6d0808 100%);
border: 1px solid #8b0000;
border-radius: 4px;
.sortilege-actor-info {
display: flex;
flex-direction: column;
gap: 3px;
}
.sortilege-actor-img {
width: 48px;
height: 48px;
border-radius: 4px;
border: 2px solid #a05020;
object-fit: cover;
flex-shrink: 0;
}
.sortilege-actor-name {
font-weight: bold;
font-size: 1.05rem;
color: #ffffff;
text-shadow: 0 1px 3px rgba(0,0,0,0.9);
}
.sortilege-ame-info {
font-size: 0.88rem;
color: #fde0ff;
text-shadow: 0 1px 2px rgba(0,0,0,0.8);
strong { color: #ffffff; font-weight: bold; }
}
}
.sortilege-mode-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
padding: 5px 4px;
border-bottom: 1px solid rgba(139, 69, 19, 0.4);
label {
flex-shrink: 0;
font-weight: bold;
color: #5a1a00;
}
select {
flex: 1;
background: #3a1a08 !important;
border: 1px solid #c06030 !important;
border-radius: 3px;
padding: 5px 8px;
color: #ffffff !important;
font-family: CentaurMT, serif;
font-size: 0.95rem;
font-weight: bold;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%23ffd070' d='M6 8L0 0h12z'/%3E%3C/svg%3E") !important;
background-repeat: no-repeat !important;
background-position: right 8px center !important;
background-size: 10px 7px !important;
padding-right: 28px;
option {
background: #1a0e06;
color: #ffffff;
font-weight: normal;
}
}
}
.sortilege-runes-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 12px;
font-size: 0.88rem;
border: 1px solid #6b3010;
border-radius: 4px;
overflow: hidden;
thead tr th {
background: #3a1808;
padding: 6px 8px;
text-align: center;
border-bottom: 2px solid #8b4513;
color: #ffd070;
font-weight: bold;
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
tbody tr.sortilege-rune-row {
border-bottom: 1px solid rgba(139, 69, 19, 0.3);
transition: background 0.15s;
background: rgba(255, 240, 210, 0.3);
&:last-child { border-bottom: none; }
&:nth-child(even) { background: rgba(200, 160, 100, 0.15); }
&:hover { background: rgba(139, 69, 19, 0.15); }
&.rune-selected {
background: rgba(139, 69, 19, 0.25);
}
td {
padding: 5px 8px;
vertical-align: middle;
color: #2a1a0a;
}
.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);
}
.sortilege-rune-points {
width: 55px;
text-align: center;
background: #3a1a08;
border: 1px solid #8b4513;
border-radius: 3px;
color: #f0e8d8;
padding: 2px 4px;
font-family: CentaurMT, serif;
cursor: pointer;
option { background: #1a0e06; color: #f0e8d8; }
&:disabled {
opacity: 0.35;
cursor: not-allowed;
}
}
}
}
.sortilege-summary {
background: linear-gradient(135deg, #4a0404 0%, #3a1808 100%);
border: 1px solid #8b4513;
border-left: 4px solid #c0392b;
border-radius: 4px;
padding: 10px 14px;
margin-bottom: 10px;
.summary-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
font-size: 0.9rem;
&:last-child { margin-bottom: 0; }
.summary-label {
font-weight: bold;
color: #f0d8b0;
text-shadow: 0 1px 2px rgba(0,0,0,0.8);
}
.summary-value {
color: #ffd700;
font-weight: bold;
font-size: 1.05rem;
text-shadow: 0 1px 3px rgba(0,0,0,0.9);
}
}
}
// Auto-cible and Modificateur rows
.flexrow {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
padding: 3px 4px;
label {
flex: 1;
color: #5a1a00;
font-size: 0.9rem;
font-weight: bold;
}
input[type="number"] {
width: 70px;
background: #3a1a08;
border: 1px solid #8b4513;
border-radius: 3px;
padding: 3px 8px;
color: #f0e8d8;
text-align: center;
font-family: CentaurMT, serif;
}
.sortilege-modificateur-select {
width: 90px;
background: #3a1a08;
border: 1px solid #8b4513;
border-radius: 3px;
padding: 4px 8px;
color: #f0e8d8;
font-family: CentaurMT, serif;
text-align: center;
cursor: pointer;
option { background: #1a0e06; color: #f0e8d8; }
}
input[type="checkbox"] {
width: 16px;
height: 16px;
flex-shrink: 0;
accent-color: #c0392b;
}
}
// ---- Invocation démon specific ----
.actor-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
padding: 6px 10px;
background: linear-gradient(135deg, #1a0030 0%, #3a0060 100%);
border: 1px solid #6a008a;
border-radius: 4px;
.actor-portrait {
width: 44px;
height: 44px;
border-radius: 4px;
border: 2px solid #9900cc;
object-fit: cover;
flex-shrink: 0;
}
h3 {
font-weight: bold;
font-size: 1.05rem;
color: #ffffff;
text-shadow: 0 1px 3px rgba(0,0,0,0.9);
margin: 0;
}
.dialog-subtitle {
font-size: 0.85rem;
color: #ddb0ff;
i { margin-right: 4px; }
}
.actor-info { display: flex; flex-direction: column; gap: 2px; }
}
.invoc-warning-box {
background: rgba(180, 20, 20, 0.15);
border: 1px solid #8b0000;
border-left: 4px solid #c0392b;
border-radius: 4px;
padding: 7px 10px;
margin-bottom: 8px;
font-size: 0.85rem;
color: #6a0000;
i { margin-right: 6px; color: #c0392b; }
strong { color: #8b0000; }
div { margin-top: 3px; }
}
.invoc-comp-summary {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 10px;
.comp-badge {
background: rgba(60, 20, 80, 0.12);
border: 1px solid rgba(120, 40, 160, 0.4);
border-radius: 12px;
padding: 3px 10px;
font-size: 0.82rem;
color: #3a0060;
&.comp-missing {
background: rgba(120, 0, 0, 0.08);
border-color: rgba(180, 0, 0, 0.3);
color: #800000;
em { font-style: italic; opacity: 0.7; }
}
strong { color: #2a004a; font-weight: bold; }
}
}
.invoc-seuil-calculator {
background: rgba(60, 10, 80, 0.07);
border: 1px solid rgba(120, 40, 160, 0.35);
border-radius: 5px;
padding: 10px 12px;
margin-bottom: 10px;
.invoc-section-title {
font-size: 0.88rem;
font-weight: bold;
color: #3a0060;
margin: 0 0 8px 0;
text-transform: uppercase;
letter-spacing: 0.5px;
i { margin-right: 5px; }
}
.invoc-criteria-grid {
display: grid;
grid-template-columns: 1fr auto;
gap: 5px 10px;
align-items: center;
margin-bottom: 10px;
label {
font-size: 0.83rem;
color: #3a1a0a;
font-weight: bold;
padding: 1px 0;
}
select, input[type="number"] {
background: #3a1a08;
border: 1px solid #8b4513;
border-radius: 3px;
padding: 4px 24px 4px 8px;
color: #f0e8d8;
font-family: CentaurMT, serif;
font-size: 0.83rem;
cursor: pointer;
min-width: 200px;
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7' viewBox='0 0 10 7'%3E%3Cpath fill='%23ffd070' d='M5 7L0 0h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 7px center;
background-size: 9px 6px;
option { background: #1a0e06; color: #f0e8d8; }
}
input[type="number"] {
min-width: 80px;
max-width: 80px;
text-align: center;
background-image: none;
padding: 4px 8px;
}
}
.invoc-seuil-total-row {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 8px;
background: linear-gradient(135deg, #2a005a 0%, #4a0080 100%);
border: 1px solid #9900cc;
border-radius: 4px;
.invoc-seuil-label {
flex: 1;
font-weight: bold;
font-size: 0.9rem;
color: #ddb0ff;
}
.invoc-seuil-total {
font-size: 1.3rem;
font-weight: bold;
color: #ffd700;
text-shadow: 0 0 8px rgba(255, 200, 0, 0.6);
min-width: 32px;
text-align: center;
}
}
}
.invoc-form-grid {
display: flex;
flex-direction: column;
gap: 5px;
.invoc-field {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 6px;
border-bottom: 1px solid rgba(139, 69, 19, 0.2);
label {
flex: 1;
font-weight: bold;
font-size: 0.87rem;
color: #3a1a0a;
}
.invoc-value-highlight {
font-size: 1.1rem;
font-weight: bold;
color: #6a0080;
min-width: 30px;
text-align: center;
}
.invoc-hint {
font-size: 0.78rem;
color: #5a3a0a;
font-style: italic;
strong { color: #8b0000; font-weight: bold; }
}
select {
background: #3a1a08;
border: 1px solid #8b4513;
border-radius: 3px;
padding: 4px 24px 4px 8px;
color: #f0e8d8;
font-family: CentaurMT, serif;
font-size: 0.85rem;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7' viewBox='0 0 10 7'%3E%3Cpath fill='%23ffd070' d='M5 7L0 0h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 7px center;
background-size: 9px 6px;
option { background: #1a0e06; color: #f0e8d8; }
}
}
}
}
// =====================================================
// Sortilège chat result
// =====================================================
.mournblade-chat-result {
.sortilege-runes-summary {
margin-top: 8px;
border-top: 1px solid rgba(139, 69, 19, 0.3);
padding-top: 6px;
.details-section-title {
font-weight: bold;
font-size: 0.85rem;
color: #4a0404;
margin-bottom: 4px;
i { margin-right: 4px; }
}
.sortilege-rune-detail {
align-items: center;
gap: 8px;
padding: 3px 0;
font-size: 0.82rem;
border-bottom: 1px solid rgba(0,0,0,0.06);
.rune-mini-img {
width: 18px;
height: 18px;
border-radius: 2px;
flex-shrink: 0;
}
.rune-name {
flex: 1;
font-weight: bold;
}
.rune-formule {
font-style: italic;
color: #5a3a8a;
font-size: 0.8rem;
white-space: nowrap;
}
.rune-pts, .rune-actions { color: #666; white-space: nowrap; }
.rune-duree { color: #4a2060; font-style: italic; white-space: nowrap; }
}
}
// Haut-Parler formule dans la rune (chat-generic-result)
.rune-formule {
font-style: italic;
color: #5a3a8a;
}
}
@@ -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
}
}
+249 -5
View File
@@ -45,20 +45,20 @@ export class MournbladeRollDialog {
label: "Lancer 1d10",
icon: "fa-solid fa-dice-d10",
default: true,
callback: (event, button, dialog) => {
callback: async (event, button, dialog) => {
this._updateRollDataFromForm(rollData, button.form.elements, actor)
rollData.mainDice = "1d10"
MournbladeUtility.rollMournblade(rollData)
await MournbladeUtility.rollMournblade(rollData)
}
},
{
action: "rolld20",
label: "Lancer 1d20",
icon: "fa-solid fa-dice-d20",
callback: (event, button, dialog) => {
callback: async (event, button, dialog) => {
this._updateRollDataFromForm(rollData, button.form.elements, actor)
rollData.mainDice = "1d20"
MournbladeUtility.rollMournblade(rollData)
await MournbladeUtility.rollMournblade(rollData)
}
},
],
@@ -67,7 +67,248 @@ export class MournbladeRollDialog {
}
/**
* Mettre à jour rollData avec les valeurs du formulaire
* 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
@@ -98,6 +339,9 @@ export class MournbladeRollDialog {
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) {
+1
View File
@@ -24,6 +24,7 @@ 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';
+126 -10
View File
@@ -45,6 +45,7 @@ export default class MournbladeActorSheet extends HandlebarsApplicationMixin(fou
editItem: MournbladeActorSheet.#onEditItem,
deleteItem: MournbladeActorSheet.#onDeleteItem,
createItem: MournbladeActorSheet.#onCreateItem,
postItem: MournbladeActorSheet.#onPostItem,
equipItem: MournbladeActorSheet.#onEquipItem,
modifyQuantity: MournbladeActorSheet.#onModifyQuantity,
modifySante: MournbladeActorSheet.#onModifySante,
@@ -52,6 +53,14 @@ export default class MournbladeActorSheet extends HandlebarsApplicationMixin(fou
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,
@@ -105,6 +114,21 @@ export default class MournbladeActorSheet extends HandlebarsApplicationMixin(fou
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)
@@ -131,22 +155,37 @@ export default class MournbladeActorSheet extends HandlebarsApplicationMixin(fou
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] || "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 => {
const tab = link.dataset.tab
link.classList.toggle('active', tab === activeTab)
link.addEventListener('click', (event) => {
event.preventDefault()
this.tabGroups[group] = tab
this.render()
switchTab(link.dataset.tab)
})
})
// Show/hide tab content
this.element.querySelectorAll('[data-group="' + group + '"][data-tab]').forEach(content => {
content.classList.toggle('active', content.dataset.tab === activeTab)
})
}
}
@@ -238,6 +277,16 @@ export default class MournbladeActorSheet extends HandlebarsApplicationMixin(fou
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
@@ -335,6 +384,73 @@ export default class MournbladeActorSheet extends HandlebarsApplicationMixin(fou
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
+72 -14
View File
@@ -32,6 +32,7 @@ export default class MournbladeItemSheet extends HandlebarsApplicationMixin(foun
actions: {
editImage: MournbladeItemSheet.#onEditImage,
postItem: MournbladeItemSheet.#onPostItem,
enchanter: MournbladeItemSheet.#onEnchanter,
},
}
@@ -43,17 +44,24 @@ export default class MournbladeItemSheet extends HandlebarsApplicationMixin(foun
/** @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: this.document.schema.fields,
systemFields: this.document.system.schema.fields,
item: this.document,
system: this.document.system,
source: this.document.toObject(),
enrichedDescription: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.description, { async: true }),
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
}
@@ -96,6 +104,19 @@ export default class MournbladeItemSheet extends HandlebarsApplicationMixin(foun
// #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
@@ -122,21 +143,58 @@ export default class MournbladeItemSheet extends HandlebarsApplicationMixin(foun
if (this.document.actor) {
chatData.actor = { id: this.document.actor.id }
}
// Don't post any image for the item if the default image is used
if (chatData.img.includes("/blank.png")) {
if (chatData.img?.includes("/blank.png")) {
chatData.img = null
}
// JSON object for easy creation
chatData.jsondata = JSON.stringify({
compendium: "postedItem",
payload: chatData,
})
const html = await foundry.applications.handlebars.renderTemplate('systems/fvtt-mournblade/templates/post-item.hbs', chatData)
const chatOptions = {
user: game.user.id,
content: html,
// 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"
}
ChatMessage.create(chatOptions)
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 })
}
}
@@ -10,6 +10,11 @@ export default class MournbladeCompetenceSheet extends MournbladeItemSheet {
window: {
contentClasses: ["competence-content"],
},
actions: {
...MournbladeItemSheet.DEFAULT_OPTIONS.actions,
addPredilection: MournbladeCompetenceSheet.#onAddPredilection,
deletePredilection: MournbladeCompetenceSheet.#onDeletePredilection,
},
}
/** @override */
@@ -46,4 +51,28 @@ export default class MournbladeCompetenceSheet extends MournbladeItemSheet {
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 })
}
}
@@ -15,6 +15,7 @@ export default class MournbladeCreatureSheet extends MournbladeActorSheet {
static PARTS = {
sheet: {
template: "systems/fvtt-mournblade/templates/creature-sheet.hbs",
scrollable: [".tab.competences", ".tab.equipement", ".tab.biodata"],
},
}
@@ -32,6 +33,7 @@ export default class MournbladeCreatureSheet extends MournbladeActorSheet {
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())
@@ -15,6 +15,7 @@ export default class MournbladePersonnageSheet extends MournbladeActorSheet {
static PARTS = {
sheet: {
template: "systems/fvtt-mournblade/templates/actor-sheet.hbs",
scrollable: [".tab.principal", ".tab.competences", ".tab.dons", ".tab.equipement"],
},
}
@@ -35,6 +36,8 @@ export default class MournbladePersonnageSheet extends MournbladeActorSheet {
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())
@@ -46,6 +49,7 @@ export default class MournbladePersonnageSheet extends MournbladeActorSheet {
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())
@@ -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
}
}
+6 -1
View File
@@ -21,7 +21,12 @@ export default class ArmeDataModel extends foundry.abstract.TypeDataModel {
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 })
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 }),
}),
};
}
}
+6 -1
View File
@@ -11,7 +11,12 @@ export default class BouclierDataModel extends foundry.abstract.TypeDataModel {
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 })
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 }),
}),
};
}
}
+1
View File
@@ -5,6 +5,7 @@ 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: "" })
};
}
+7
View File
@@ -10,6 +10,13 @@ export default class CreatureDataModel extends foundry.abstract.TypeDataModel {
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: "" }),
+6 -1
View File
@@ -7,7 +7,12 @@ export default class EquipementDataModel extends foundry.abstract.TypeDataModel
return {
description: new fields.HTMLField({ initial: "" }),
rarete: new fields.NumberField({ initial: 0, integer: true }),
prix: 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 }),
}),
};
}
}
+1
View File
@@ -19,6 +19,7 @@ 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';
+2
View File
@@ -80,6 +80,8 @@ export default class PersonnageDataModel extends foundry.abstract.TypeDataModel
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 }),
+24
View File
@@ -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: "" }),
};
}
}
+6 -1
View File
@@ -11,7 +11,12 @@ export default class ProtectionDataModel extends foundry.abstract.TypeDataModel
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 })
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 }),
}),
};
}
}
+208 -18
View File
@@ -1,6 +1,10 @@
/* -------------------------------------------- */
import { MournbladeUtility } from "./mournblade-utility.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]
@@ -90,6 +94,8 @@ 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
let competence = this.items.find(item => item.type == "competence" && item.name.toLowerCase() == "mêlée")
@@ -97,7 +103,7 @@ export class MournbladeActor extends Actor {
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
}
@@ -114,7 +120,7 @@ export class MournbladeActor extends Actor {
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
@@ -132,12 +138,14 @@ export class MournbladeActor extends Actor {
prepareBouclier(bouclier) {
bouclier = foundry.utils.duplicate(bouclier)
let combat = this.getCombatValues()
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
@@ -212,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"])
}
@@ -280,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))]
}
/* -------------------------------------------- */
@@ -382,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()
@@ -403,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 })
}
}
@@ -425,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 })
}
/* -------------------------------------------- */
@@ -437,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 })
}
/* -------------------------------------------- */
@@ -455,7 +469,7 @@ export class MournbladeActor extends Actor {
} else {
ame.currentmax -= value
}
this.update({ 'system.ame': ame })
return this.update({ 'system.ame': ame })
}
/* -------------------------------------------- */
@@ -471,7 +485,7 @@ export class MournbladeActor extends Actor {
/* -------------------------------------------- */
getAttribute(attrKey) {
return this.system.attributes[attrKey]
return this.system.attributs[attrKey]
}
/* -------------------------------------------- */
@@ -479,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))]
}
/* -------------------------------------------- */
@@ -650,6 +664,175 @@ export class MournbladeActor extends Actor {
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)
}
/* -------------------------------------------- */
async rollArmeOffensif(armeId) {
let arme = this.items.get(armeId)
@@ -699,7 +882,7 @@ export class MournbladeActor extends Actor {
if (rollData.defender) {
rollData.selectDifficulte = false
let comp = rollData.defender.items.find(it => it.type == "competence" && it.name.toLowerCase() == "mouvements")
rollData.difficulte = rollData.defender.system.attributs.adr.value + ((comp) ? comp.system.niveau : rollData.defender.system.attributs.adr.value)
rollData.difficulte = rollData.defender.system.attributs.adr.value + ((comp) ? comp.system.niveau : rollData.defender.system.attributs.adr.value)
}
console.log("Fuir!", rollData)
await MournbladeRollDialog.create(this, rollData)
@@ -738,10 +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).evaluate()
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,
degatsFormula: arme.system.totalDegats,
arme: arme,
finalResult: roll.total,
alias: this.name,
+68
View File
@@ -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"),
}
}
+15 -14
View File
@@ -1,20 +1,21 @@
import { MournbladeUtility } from "./mournblade-utility.js";
export const defaultItemImg = {
competence: "systems/fvtt-mournblade/assets/icons/competence.webp",
arme: "systems/fvtt-mournblade/assets/icons/arme.webp",
capacite: "systems/fvtt-mournblade/assets/icons/capacite.webp",
don: "systems/fvtt-mournblade/assets/icons/don.webp",
equipement: "systems/fvtt-mournblade/assets/icons/equipement.webp",
monnaie: "systems/fvtt-mournblade/assets/icons/monnaie.webp",
pacte: "systems/fvtt-mournblade/assets/icons/pacte.webp",
predilection: "systems/fvtt-mournblade/assets/icons/predilection.webp",
protection: "systems/fvtt-mournblade/assets/icons/protection.webp",
rune: "systems/fvtt-mournblade/assets/icons/rune.webp",
runeeffect: "systems/fvtt-mournblade/assets/icons/rune.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"
competence: "systems/fvtt-mournblade/assets/icons/competence.webp",
arme: "systems/fvtt-mournblade/assets/icons/arme.webp",
capacite: "systems/fvtt-mournblade/assets/icons/capacite.webp",
don: "systems/fvtt-mournblade/assets/icons/don.webp",
equipement: "systems/fvtt-mournblade/assets/icons/equipement.webp",
monnaie: "systems/fvtt-mournblade/assets/icons/monnaie.webp",
pacte: "systems/fvtt-mournblade/assets/icons/pacte.webp",
predilection: "systems/fvtt-mournblade/assets/icons/predilection.webp",
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",
}
/**
+6 -4
View File
@@ -74,6 +74,7 @@ Hooks.once("init", async function () {
protection: models.ProtectionDataModel,
rune: models.RuneDataModel,
runeeffect: models.RuneEffectDataModel,
potion: models.PotionDataModel,
tendance: models.TendanceDataModel,
traitchaotique: models.TraitChaotiqueDataModel,
traitespece: models.TraitEspeceDataModel
@@ -108,6 +109,7 @@ Hooks.once("init", async function () {
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 });
@@ -121,7 +123,7 @@ async function welcomeMessage() {
const templateData = {};
const html = await foundry.applications.handlebars.renderTemplate("systems/fvtt-mournblade/templates/chat-welcome-message.hbs", templateData);
ChatMessage.create({
await ChatMessage.create({
user: game.user.id,
whisper: [game.user.id],
content: html
@@ -142,7 +144,7 @@ async function importDefaultScene() {
/* -------------------------------------------- */
/* Foundry VTT Initialization */
/* -------------------------------------------- */
Hooks.once("ready", function () {
Hooks.once("ready", async function () {
game.system.mournblade = {
config : MournbladeConfig.getConfig(),
@@ -152,14 +154,14 @@ 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
});
}
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
});
+648 -8
View File
@@ -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)
@@ -157,6 +163,52 @@ export class MournbladeUtility {
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 })
}
/* -------------------------------------------- */
@@ -166,7 +218,16 @@ export class MournbladeUtility {
'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-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);
}
@@ -391,12 +452,16 @@ export class MournbladeUtility {
}
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`
}
}
@@ -412,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
@@ -436,6 +543,99 @@ export class MournbladeUtility {
}
/* -------------------------------------------- */
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
@@ -684,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()
@@ -875,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)
})
}
}
+739
View File
@@ -9,11 +9,164 @@
"version": "1.0.0",
"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"
}
},
"node_modules/@foundryvtt/foundryvtt-cli": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@foundryvtt/foundryvtt-cli/-/foundryvtt-cli-3.0.3.tgz",
"integrity": "sha512-byNLOrZ9ev6PfW+gflhonT5Y+Goq2aHERwVGWO3eYRKez+lzZJoqMY/YMEi54XYozzWWn8d6LuRbBsVPJ30haw==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^5.4.1",
"classic-level": "^1.4.1",
"esm": "^3.2.25",
"js-yaml": "^4.1.0",
"mkdirp": "^3.0.1",
"nedb-promises": "^6.2.3",
"yargs": "^17.7.2"
},
"bin": {
"fvtt": "fvtt.mjs"
},
"engines": {
"node": ">17.0.0"
}
},
"node_modules/@foundryvtt/foundryvtt-cli/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/@foundryvtt/foundryvtt-cli/node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@foundryvtt/foundryvtt-cli/node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/@foundryvtt/foundryvtt-cli/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/@foundryvtt/foundryvtt-cli/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@foundryvtt/foundryvtt-cli/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@foundryvtt/foundryvtt-cli/node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/@foundryvtt/foundryvtt-cli/node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/@foundryvtt/foundryvtt-cli/node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@foundryvtt/foundryvtt-cli/node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/@gulp-sourcemaps/identity-map": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz",
@@ -80,6 +233,67 @@
"xtend": "~4.0.1"
}
},
"node_modules/@seald-io/binary-search-tree": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@seald-io/binary-search-tree/-/binary-search-tree-1.0.3.tgz",
"integrity": "sha512-qv3jnwoakeax2razYaMsGI/luWdliBLHTdC6jU55hQt1hcFqzauH/HsBollQ7IR4ySTtYhT+xyHoijpA16C+tA==",
"dev": true
},
"node_modules/@seald-io/nedb": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@seald-io/nedb/-/nedb-4.1.2.tgz",
"integrity": "sha512-bDr6TqjBVS2rDyYM9CPxAnotj5FuNL9NF8o7h7YyFXM7yruqT4ddr+PkSb2mJvvw991bqdftazkEo38gykvaww==",
"dev": true,
"license": "MIT",
"dependencies": {
"@seald-io/binary-search-tree": "^1.0.3",
"localforage": "^1.10.0",
"util": "^0.12.5"
}
},
"node_modules/abstract-level": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/abstract-level/-/abstract-level-1.0.4.tgz",
"integrity": "sha512-eUP/6pbXBkMbXFdx4IH2fVgvB7M0JvR7/lIL33zcs0IBcwjdzSSl31TOJsaCzmKSSDF9h8QYSOJux4Nd4YJqFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer": "^6.0.3",
"catering": "^2.1.0",
"is-buffer": "^2.0.5",
"level-supports": "^4.0.0",
"level-transcoder": "^1.0.1",
"module-error": "^1.0.1",
"queue-microtask": "^1.2.3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/abstract-level/node_modules/is-buffer": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/acorn": {
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz",
@@ -129,6 +343,22 @@
"node": ">=0.10.0"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/ansi-wrap": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz",
@@ -183,6 +413,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/arr-diff": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@@ -396,6 +633,22 @@
"node": ">= 4.5.0"
}
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"possible-typed-array-names": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/bach": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz",
@@ -456,6 +709,27 @@
"node": ">=0.10.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/binary-extensions": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz",
@@ -510,6 +784,31 @@
"node": ">=0.10.0"
}
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/buffer-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz",
@@ -611,6 +910,29 @@
"node": ">=0.10.0"
}
},
"node_modules/catering": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/catering/-/catering-2.1.1.tgz",
"integrity": "sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chokidar": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz",
@@ -677,6 +999,24 @@
"node": ">= 0.4"
}
},
"node_modules/classic-level": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/classic-level/-/classic-level-1.4.1.tgz",
"integrity": "sha512-qGx/KJl3bvtOHrGau2WklEZuXhS3zme+jf+fsu6Ej7W7IP/C49v7KNlWIsT1jZu0YnfzSIYDGcEWpCa1wKGWXQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"abstract-level": "^1.0.2",
"catering": "^2.1.0",
"module-error": "^1.0.1",
"napi-macros": "^2.2.2",
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/cliui": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
@@ -767,6 +1107,26 @@
"node": ">=0.10.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
@@ -1071,6 +1431,13 @@
"node": ">=0.10.0"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
@@ -1194,6 +1561,26 @@
"es6-symbol": "^3.1.1"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/esm": {
"version": "3.2.25",
"resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
"integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/esniff": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
@@ -1488,6 +1875,22 @@
"readable-stream": "^2.3.6"
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-callable": "^1.2.7"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/for-in": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@@ -1586,6 +1989,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/generator-function": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
"integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-caller-file": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
@@ -1937,6 +2350,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
@@ -2026,6 +2455,27 @@
"node": ">=0.10.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/image-size": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
@@ -2040,6 +2490,13 @@
"node": ">=0.10.0"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"dev": true,
"license": "MIT"
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -2113,6 +2570,23 @@
"node": ">= 0.10"
}
},
"node_modules/is-arguments": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
@@ -2140,6 +2614,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/is-callable": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
@@ -2216,6 +2703,26 @@
"node": ">=0.10.0"
}
},
"node_modules/is-generator-function": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
"integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.4",
"generator-function": "^2.0.0",
"get-proto": "^1.0.1",
"has-tostringtag": "^1.0.2",
"safe-regex-test": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -2282,6 +2789,25 @@
"dev": true,
"license": "MIT"
},
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-relative": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
@@ -2295,6 +2821,22 @@
"node": ">=0.10.0"
}
},
"node_modules/is-typed-array": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"which-typed-array": "^1.1.16"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-unc-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz",
@@ -2366,6 +2908,19 @@
"node": ">=0.10.0"
}
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
@@ -2471,6 +3026,40 @@
"source-map": "~0.6.0"
}
},
"node_modules/level-supports": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/level-supports/-/level-supports-4.0.1.tgz",
"integrity": "sha512-PbXpve8rKeNcZ9C1mUicC9auIYFyGpkV9/i6g76tLgANwWhtG2v7I4xNBUlkn3lE2/dZF3Pi0ygYGtLc4RXXdA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/level-transcoder": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/level-transcoder/-/level-transcoder-1.0.1.tgz",
"integrity": "sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer": "^6.0.3",
"module-error": "^1.0.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/lie": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
"integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
"dev": true,
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/liftoff": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz",
@@ -2531,6 +3120,16 @@
"node": ">=0.10.0"
}
},
"node_modules/localforage": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"lie": "3.1.1"
}
},
"node_modules/lru-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz",
@@ -2819,6 +3418,32 @@
"node": ">=0.10.0"
}
},
"node_modules/mkdirp": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
"dev": true,
"license": "MIT",
"bin": {
"mkdirp": "dist/cjs/src/bin.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/module-error": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/module-error/-/module-error-1.0.2.tgz",
"integrity": "sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -2917,6 +3542,23 @@
"node": ">=0.10.0"
}
},
"node_modules/napi-macros": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.2.2.tgz",
"integrity": "sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==",
"dev": true,
"license": "MIT"
},
"node_modules/nedb-promises": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/nedb-promises/-/nedb-promises-6.2.3.tgz",
"integrity": "sha512-enq0IjNyBz9Qy9W/QPCcLGh/QORGBjXbIeZeWvIjO3OMLyAvlKT3hiJubP2BKEiFniUlR3L01o18ktqgn5jxqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@seald-io/nedb": "^4.0.2"
}
},
"node_modules/needle": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz",
@@ -2942,6 +3584,18 @@
"dev": true,
"license": "ISC"
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"dev": true,
"license": "MIT",
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/normalize-package-data": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
@@ -3437,6 +4091,16 @@
"node": ">=0.10.0"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/postcss": {
"version": "7.0.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz",
@@ -3503,6 +4167,27 @@
"pump": "^2.0.0"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/read-pkg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
@@ -3821,6 +4506,24 @@
"ret": "~0.1.10"
}
},
"node_modules/safe-regex-test": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"is-regex": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -4712,6 +5415,20 @@
"node": ">=0.10.0"
}
},
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"is-arguments": "^1.0.4",
"is-generator-function": "^1.0.7",
"is-typed-array": "^1.1.3",
"which-typed-array": "^1.1.2"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -4893,6 +5610,28 @@
"dev": true,
"license": "ISC"
},
"node_modules/which-typed-array": {
"version": "1.1.20",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
"integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
"dev": true,
"license": "MIT",
"dependencies": {
"available-typed-arrays": "^1.0.7",
"call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"for-each": "^0.3.5",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/wrap-ansi": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+1
View File
@@ -9,6 +9,7 @@
"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
View File
@@ -1 +1 @@
MANIFEST-000304
MANIFEST-000404
+7 -8
View File
@@ -1,8 +1,7 @@
2026/01/09-16:51:34.709189 7f1c56bff6c0 Recovering log #302
2026/01/09-16:51:34.720652 7f1c56bff6c0 Delete type=3 #300
2026/01/09-16:51:34.720827 7f1c56bff6c0 Delete type=0 #302
2026/01/09-17:10:52.171232 7f1c54bfb6c0 Level-0 table #307: started
2026/01/09-17:10:52.171270 7f1c54bfb6c0 Level-0 table #307: 0 bytes OK
2026/01/09-17:10:52.181182 7f1c54bfb6c0 Delete type=0 #305
2026/01/09-17:10:52.181380 7f1c54bfb6c0 Manual compaction at level-0 from '!items!0swiE8k5zfUIqmXu' @ 72057594037927935 : 1 .. '!items!wv5EiePmPTpqFutt' @ 0 : 0; will stop at (end)
2026/01/09-17:10:52.181415 7f1c54bfb6c0 Manual compaction at level-1 from '!items!0swiE8k5zfUIqmXu' @ 72057594037927935 : 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)
+7 -8
View File
@@ -1,8 +1,7 @@
2026/01/09-16:50:28.487537 7f1c56bff6c0 Recovering log #298
2026/01/09-16:50:28.498450 7f1c56bff6c0 Delete type=3 #296
2026/01/09-16:50:28.498515 7f1c56bff6c0 Delete type=0 #298
2026/01/09-16:51:28.840653 7f1c54bfb6c0 Level-0 table #303: started
2026/01/09-16:51:28.840708 7f1c54bfb6c0 Level-0 table #303: 0 bytes OK
2026/01/09-16:51:28.847927 7f1c54bfb6c0 Delete type=0 #301
2026/01/09-16:51:28.868475 7f1c54bfb6c0 Manual compaction at level-0 from '!items!0swiE8k5zfUIqmXu' @ 72057594037927935 : 1 .. '!items!wv5EiePmPTpqFutt' @ 0 : 0; will stop at (end)
2026/01/09-16:51:28.868551 7f1c54bfb6c0 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)
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
MANIFEST-000023
+15
View File
@@ -0,0 +1,15 @@
2026/05/02-23:45:02.567239 7fd7557ee6c0 Recovering log #20
2026/05/02-23:45:02.577782 7fd7557ee6c0 Delete type=3 #6
2026/05/02-23:45:02.577851 7fd7557ee6c0 Delete type=0 #20
2026/05/02-23:46:06.389308 7fd7477fe6c0 Level-0 table #26: started
2026/05/02-23:46:06.404009 7fd7477fe6c0 Level-0 table #26: 34677 bytes OK
2026/05/02-23:46:06.435632 7fd7477fe6c0 Delete type=0 #24
2026/05/02-23:46:06.491707 7fd7477fe6c0 Manual compaction at level-0 from '!actors!AutomCombatMaj001' @ 72057594037927935 : 1 .. '!actors.items!tavgPDu6zINLsWlv' @ 0 : 0; will stop at (end)
2026/05/02-23:46:06.526199 7fd7477fe6c0 Manual compaction at level-1 from '!actors!AutomCombatMaj001' @ 72057594037927935 : 1 .. '!actors.items!tavgPDu6zINLsWlv' @ 0 : 0; will stop at '!actors.items!tavgPDu6zINLsWlv' @ 363 : 1
2026/05/02-23:46:06.526225 7fd7477fe6c0 Compacting 1@1 + 1@2 files
2026/05/02-23:46:06.549927 7fd7477fe6c0 Generated table #27@1: 190 keys, 46363 bytes
2026/05/02-23:46:06.549995 7fd7477fe6c0 Compacted 1@1 + 1@2 files => 46363 bytes
2026/05/02-23:46:06.579766 7fd7477fe6c0 compacted to: files[ 0 0 1 0 0 0 0 ]
2026/05/02-23:46:06.579952 7fd7477fe6c0 Delete type=2 #22
2026/05/02-23:46:06.580109 7fd7477fe6c0 Delete type=2 #26
2026/05/02-23:46:06.648571 7fd7477fe6c0 Manual compaction at level-1 from '!actors.items!tavgPDu6zINLsWlv' @ 363 : 1 .. '!actors.items!tavgPDu6zINLsWlv' @ 0 : 0; will stop at (end)
+18
View File
@@ -0,0 +1,18 @@
2026/05/02-23:32:06.900144 7fd754fed6c0 Recovering log #4
2026/05/02-23:32:06.900180 7fd754fed6c0 Recovering log #18
2026/05/02-23:32:07.070840 7fd754fed6c0 Delete type=0 #18
2026/05/02-23:32:07.070915 7fd754fed6c0 Delete type=2 #20
2026/05/02-23:32:07.070972 7fd754fed6c0 Delete type=0 #4
2026/05/02-23:32:07.071012 7fd754fed6c0 Delete type=3 #2
2026/05/02-23:32:54.156045 7fd7477fe6c0 Level-0 table #21: started
2026/05/02-23:32:54.159838 7fd7477fe6c0 Level-0 table #21: 44646 bytes OK
2026/05/02-23:32:54.166019 7fd7477fe6c0 Delete type=0 #19
2026/05/02-23:32:54.182233 7fd7477fe6c0 Manual compaction at level-0 from '!actors!AutomCombatMaj001' @ 72057594037927935 : 1 .. '!actors.items!j8cTuVFIW4IhAe50' @ 0 : 0; will stop at (end)
2026/05/02-23:32:54.192353 7fd7477fe6c0 Manual compaction at level-1 from '!actors!AutomCombatMaj001' @ 72057594037927935 : 1 .. '!actors.items!j8cTuVFIW4IhAe50' @ 0 : 0; will stop at '!actors.items!j8cTuVFIW4IhAe50' @ 163 : 1
2026/05/02-23:32:54.192362 7fd7477fe6c0 Compacting 1@1 + 1@2 files
2026/05/02-23:32:54.196480 7fd7477fe6c0 Generated table #22@1: 190 keys, 47075 bytes
2026/05/02-23:32:54.196522 7fd7477fe6c0 Compacted 1@1 + 1@2 files => 47075 bytes
2026/05/02-23:32:54.203628 7fd7477fe6c0 compacted to: files[ 0 0 1 0 0 0 0 ]
2026/05/02-23:32:54.203745 7fd7477fe6c0 Delete type=2 #5
2026/05/02-23:32:54.203876 7fd7477fe6c0 Delete type=2 #21
2026/05/02-23:32:54.223974 7fd7477fe6c0 Manual compaction at level-1 from '!actors.items!j8cTuVFIW4IhAe50' @ 163 : 1 .. '!actors.items!j8cTuVFIW4IhAe50' @ 0 : 0; will stop at (end)
Binary file not shown.
@@ -0,0 +1 @@
MANIFEST-000011
@@ -0,0 +1,8 @@
2026/05/02-23:45:02.583541 7fd754fed6c0 Recovering log #8
2026/05/02-23:45:02.593003 7fd754fed6c0 Delete type=3 #6
2026/05/02-23:45:02.593102 7fd754fed6c0 Delete type=0 #8
2026/05/02-23:46:06.580193 7fd7477fe6c0 Level-0 table #14: started
2026/05/02-23:46:06.580241 7fd7477fe6c0 Level-0 table #14: 0 bytes OK
2026/05/02-23:46:06.608763 7fd7477fe6c0 Delete type=0 #12
2026/05/02-23:46:06.648592 7fd7477fe6c0 Manual compaction at level-0 from '!items!CapAutoComAbs001' @ 72057594037927935 : 1 .. '!items!CapAutoVoyVit001' @ 0 : 0; will stop at (end)
2026/05/02-23:46:06.695553 7fd7477fe6c0 Manual compaction at level-1 from '!items!CapAutoComAbs001' @ 72057594037927935 : 1 .. '!items!CapAutoVoyVit001' @ 0 : 0; will stop at (end)
@@ -0,0 +1,15 @@
2026/05/02-23:32:07.100528 7fd754fed6c0 Recovering log #4
2026/05/02-23:32:07.201392 7fd754fed6c0 Delete type=0 #4
2026/05/02-23:32:07.201464 7fd754fed6c0 Delete type=3 #2
2026/05/02-23:32:54.172273 7fd7477fe6c0 Level-0 table #9: started
2026/05/02-23:32:54.175326 7fd7477fe6c0 Level-0 table #9: 4810 bytes OK
2026/05/02-23:32:54.181941 7fd7477fe6c0 Delete type=0 #7
2026/05/02-23:32:54.192338 7fd7477fe6c0 Manual compaction at level-0 from '!items!CapAutoComAbs001' @ 72057594037927935 : 1 .. '!items!CapAutoVoyVit001' @ 0 : 0; will stop at (end)
2026/05/02-23:32:54.213290 7fd7477fe6c0 Manual compaction at level-1 from '!items!CapAutoComAbs001' @ 72057594037927935 : 1 .. '!items!CapAutoVoyVit001' @ 0 : 0; will stop at '!items!CapAutoVoyVit001' @ 30 : 1
2026/05/02-23:32:54.213299 7fd7477fe6c0 Compacting 1@1 + 1@2 files
2026/05/02-23:32:54.216790 7fd7477fe6c0 Generated table #10@1: 15 keys, 4810 bytes
2026/05/02-23:32:54.216823 7fd7477fe6c0 Compacted 1@1 + 1@2 files => 4810 bytes
2026/05/02-23:32:54.223591 7fd7477fe6c0 compacted to: files[ 0 0 1 0 0 0 0 ]
2026/05/02-23:32:54.223728 7fd7477fe6c0 Delete type=2 #5
2026/05/02-23:32:54.223895 7fd7477fe6c0 Delete type=2 #9
2026/05/02-23:32:54.234279 7fd7477fe6c0 Manual compaction at level-1 from '!items!CapAutoVoyVit001' @ 30 : 1 .. '!items!CapAutoVoyVit001' @ 0 : 0; will stop at (end)
+1
View File
@@ -0,0 +1 @@
MANIFEST-000006
+7
View File
@@ -0,0 +1,7 @@
2026/05/01-23:30:18.177776 7fd755fef6c0 Recovering log #4
2026/05/01-23:30:18.192738 7fd755fef6c0 Delete type=3 #2
2026/05/01-23:30:18.192830 7fd755fef6c0 Delete type=0 #4
2026/05/01-23:30:57.358830 7fd7477fe6c0 Level-0 table #9: started
2026/05/01-23:30:57.358954 7fd7477fe6c0 Level-0 table #9: 0 bytes OK
2026/05/01-23:30:57.365068 7fd7477fe6c0 Delete type=0 #7
2026/05/01-23:30:57.384807 7fd7477fe6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
+5
View File
@@ -0,0 +1,5 @@
2026/05/01-23:21:09.633678 7fd7557ee6c0 Delete type=3 #1
2026/05/01-23:30:12.668808 7fd7477fe6c0 Level-0 table #5: started
2026/05/01-23:30:12.668939 7fd7477fe6c0 Level-0 table #5: 0 bytes OK
2026/05/01-23:30:12.741575 7fd7477fe6c0 Delete type=0 #3
2026/05/01-23:30:12.842987 7fd7477fe6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
Binary file not shown.
@@ -0,0 +1 @@
MANIFEST-000057
@@ -0,0 +1,14 @@
2026/05/02-23:45:02.539275 7fd7557ee6c0 Recovering log #54
2026/05/02-23:45:02.549992 7fd7557ee6c0 Delete type=3 #52
2026/05/02-23:45:02.550092 7fd7557ee6c0 Delete type=0 #54
2026/05/02-23:46:06.455913 7fd7477fe6c0 Level-0 table #60: started
2026/05/02-23:46:06.472654 7fd7477fe6c0 Level-0 table #60: 17662 bytes OK
2026/05/02-23:46:06.491535 7fd7477fe6c0 Delete type=0 #58
2026/05/02-23:46:06.491736 7fd7477fe6c0 Manual compaction at level-0 from '!actors!ElemAirMaj0000003' @ 72057594037927935 : 1 .. '!actors.items!x8DVixbdPMJRT4g6' @ 0 : 0; will stop at '!actors.items!x8DVixbdPMJRT4g6' @ 653 : 1
2026/05/02-23:46:06.491747 7fd7477fe6c0 Compacting 1@0 + 1@1 files
2026/05/02-23:46:06.500117 7fd7477fe6c0 Generated table #61@0: 61 keys, 12816 bytes
2026/05/02-23:46:06.500186 7fd7477fe6c0 Compacted 1@0 + 1@1 files => 12816 bytes
2026/05/02-23:46:06.525751 7fd7477fe6c0 compacted to: files[ 0 1 0 0 0 0 0 ]
2026/05/02-23:46:06.525895 7fd7477fe6c0 Delete type=2 #56
2026/05/02-23:46:06.526088 7fd7477fe6c0 Delete type=2 #60
2026/05/02-23:46:06.648552 7fd7477fe6c0 Manual compaction at level-0 from '!actors.items!x8DVixbdPMJRT4g6' @ 653 : 1 .. '!actors.items!x8DVixbdPMJRT4g6' @ 0 : 0; will stop at (end)
@@ -0,0 +1,14 @@
2026/05/02-23:32:06.722004 7fd754fed6c0 Recovering log #49
2026/05/02-23:32:06.800579 7fd754fed6c0 Delete type=3 #47
2026/05/02-23:32:06.800651 7fd754fed6c0 Delete type=0 #49
2026/05/02-23:32:54.126217 7fd7477fe6c0 Level-0 table #55: started
2026/05/02-23:32:54.129656 7fd7477fe6c0 Level-0 table #55: 17683 bytes OK
2026/05/02-23:32:54.136086 7fd7477fe6c0 Delete type=0 #53
2026/05/02-23:32:54.136283 7fd7477fe6c0 Manual compaction at level-0 from '!actors!ElemAirMaj0000003' @ 72057594037927935 : 1 .. '!actors.items!wNgz4PkzpauGQRlW' @ 0 : 0; will stop at '!actors.items!xgdbv9heAyLXGkCu' @ 576 : 0
2026/05/02-23:32:54.136292 7fd7477fe6c0 Compacting 1@0 + 1@1 files
2026/05/02-23:32:54.140164 7fd7477fe6c0 Generated table #56@0: 61 keys, 12819 bytes
2026/05/02-23:32:54.140193 7fd7477fe6c0 Compacted 1@0 + 1@1 files => 12819 bytes
2026/05/02-23:32:54.146060 7fd7477fe6c0 compacted to: files[ 0 1 0 0 0 0 0 ]
2026/05/02-23:32:54.146186 7fd7477fe6c0 Delete type=2 #51
2026/05/02-23:32:54.146323 7fd7477fe6c0 Delete type=2 #55
2026/05/02-23:32:54.156031 7fd7477fe6c0 Manual compaction at level-0 from '!actors.items!xgdbv9heAyLXGkCu' @ 576 : 0 .. '!actors.items!wNgz4PkzpauGQRlW' @ 0 : 0; will stop at (end)
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
MANIFEST-000068
+15
View File
@@ -0,0 +1,15 @@
2026/05/03-10:03:45.526027 7f448d3fd6c0 Recovering log #65
2026/05/03-10:03:45.615546 7f448d3fd6c0 Delete type=3 #63
2026/05/03-10:03:45.615600 7f448d3fd6c0 Delete type=0 #65
2026/05/03-10:03:45.620364 7f4487fff6c0 Level-0 table #71: started
2026/05/03-10:03:45.646593 7f4487fff6c0 Level-0 table #71: 63615 bytes OK
2026/05/03-10:03:45.712205 7f4487fff6c0 Delete type=0 #69
2026/05/03-10:03:45.712506 7f4487fff6c0 Manual compaction at level-0 from '!actors!DemonAilesN00001' @ 72057594037927935 : 1 .. '!actors.items!DemonVoyageMin01.DemonVoyagSUR002' @ 0 : 0; will stop at (end)
2026/05/03-10:03:45.712546 7f4487fff6c0 Manual compaction at level-1 from '!actors!DemonAilesN00001' @ 72057594037927935 : 1 .. '!actors.items!DemonVoyageMin01.DemonVoyagSUR002' @ 0 : 0; will stop at '!actors.items!DemonVoyageMin01.DemonVoyagSUR002' @ 828 : 1
2026/05/03-10:03:45.712556 7f4487fff6c0 Compacting 1@1 + 1@2 files
2026/05/03-10:03:45.746878 7f4487fff6c0 Generated table #72@1: 202 keys, 63615 bytes
2026/05/03-10:03:45.746926 7f4487fff6c0 Compacted 1@1 + 1@2 files => 63615 bytes
2026/05/03-10:03:45.808622 7f4487fff6c0 compacted to: files[ 0 0 1 0 0 0 0 ]
2026/05/03-10:03:45.808775 7f4487fff6c0 Delete type=2 #67
2026/05/03-10:03:45.808903 7f4487fff6c0 Delete type=2 #71
2026/05/03-10:03:45.808992 7f4487fff6c0 Manual compaction at level-1 from '!actors.items!DemonVoyageMin01.DemonVoyagSUR002' @ 828 : 1 .. '!actors.items!DemonVoyageMin01.DemonVoyagSUR002' @ 0 : 0; will stop at (end)
+15
View File
@@ -0,0 +1,15 @@
2026/05/03-09:58:03.326790 7f78daffd6c0 Recovering log #60
2026/05/03-09:58:03.379081 7f78daffd6c0 Delete type=3 #58
2026/05/03-09:58:03.379136 7f78daffd6c0 Delete type=0 #60
2026/05/03-09:58:03.383343 7f78d9ffb6c0 Level-0 table #66: started
2026/05/03-09:58:03.402481 7f78d9ffb6c0 Level-0 table #66: 55641 bytes OK
2026/05/03-09:58:03.440377 7f78d9ffb6c0 Delete type=0 #64
2026/05/03-09:58:03.440601 7f78d9ffb6c0 Manual compaction at level-0 from '!actors!DemonCombatMaj001' @ 72057594037927935 : 1 .. '!actors.items!DemonVoyageMin01.DemonVoyagSUR002' @ 0 : 0; will stop at (end)
2026/05/03-09:58:03.440637 7f78d9ffb6c0 Manual compaction at level-1 from '!actors!DemonCombatMaj001' @ 72057594037927935 : 1 .. '!actors.items!DemonVoyageMin01.DemonVoyagSUR002' @ 0 : 0; will stop at '!actors.items!DemonVoyageMin01.DemonVoyagSUR002' @ 642 : 1
2026/05/03-09:58:03.440646 7f78d9ffb6c0 Compacting 1@1 + 1@2 files
2026/05/03-09:58:03.458233 7f78d9ffb6c0 Generated table #67@1: 179 keys, 55641 bytes
2026/05/03-09:58:03.458267 7f78d9ffb6c0 Compacted 1@1 + 1@2 files => 55641 bytes
2026/05/03-09:58:03.495173 7f78d9ffb6c0 compacted to: files[ 0 0 1 0 0 0 0 ]
2026/05/03-09:58:03.495303 7f78d9ffb6c0 Delete type=2 #62
2026/05/03-09:58:03.495434 7f78d9ffb6c0 Delete type=2 #66
2026/05/03-09:58:03.495531 7f78d9ffb6c0 Manual compaction at level-1 from '!actors.items!DemonVoyageMin01.DemonVoyagSUR002' @ 642 : 1 .. '!actors.items!DemonVoyageMin01.DemonVoyagSUR002' @ 0 : 0; will stop at (end)
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000303
MANIFEST-000403
+7 -8
View File
@@ -1,8 +1,7 @@
2026/01/09-16:51:34.750271 7f1c55bfd6c0 Recovering log #301
2026/01/09-16:51:34.760726 7f1c55bfd6c0 Delete type=3 #299
2026/01/09-16:51:34.760813 7f1c55bfd6c0 Delete type=0 #301
2026/01/09-17:10:52.202603 7f1c54bfb6c0 Level-0 table #306: started
2026/01/09-17:10:52.202692 7f1c54bfb6c0 Level-0 table #306: 0 bytes OK
2026/01/09-17:10:52.216180 7f1c54bfb6c0 Delete type=0 #304
2026/01/09-17:10:52.229242 7f1c54bfb6c0 Manual compaction at level-0 from '!items!5dGXNiL3WN4cAk7X' @ 72057594037927935 : 1 .. '!items!zzz9JrtWjELdoAfK' @ 0 : 0; will stop at (end)
2026/01/09-17:10:52.229348 7f1c54bfb6c0 Manual compaction at level-1 from '!items!5dGXNiL3WN4cAk7X' @ 72057594037927935 : 1 .. '!items!zzz9JrtWjELdoAfK' @ 0 : 0; will stop at (end)
2026/05/02-23:45:02.683455 7fd7557ee6c0 Recovering log #401
2026/05/02-23:45:02.693052 7fd7557ee6c0 Delete type=3 #399
2026/05/02-23:45:02.693123 7fd7557ee6c0 Delete type=0 #401
2026/05/02-23:46:06.987043 7fd7477fe6c0 Level-0 table #406: started
2026/05/02-23:46:06.987080 7fd7477fe6c0 Level-0 table #406: 0 bytes OK
2026/05/02-23:46:07.009855 7fd7477fe6c0 Delete type=0 #404
2026/05/02-23:46:07.091762 7fd7477fe6c0 Manual compaction at level-0 from '!items!5dGXNiL3WN4cAk7X' @ 72057594037927935 : 1 .. '!items!zzz9JrtWjELdoAfK' @ 0 : 0; will stop at (end)
+7 -8
View File
@@ -1,8 +1,7 @@
2026/01/09-16:50:28.529034 7f1c56bff6c0 Recovering log #297
2026/01/09-16:50:28.539837 7f1c56bff6c0 Delete type=3 #295
2026/01/09-16:50:28.539910 7f1c56bff6c0 Delete type=0 #297
2026/01/09-16:51:28.861880 7f1c54bfb6c0 Level-0 table #302: started
2026/01/09-16:51:28.861911 7f1c54bfb6c0 Level-0 table #302: 0 bytes OK
2026/01/09-16:51:28.868272 7f1c54bfb6c0 Delete type=0 #300
2026/01/09-16:51:28.868530 7f1c54bfb6c0 Manual compaction at level-0 from '!items!5dGXNiL3WN4cAk7X' @ 72057594037927935 : 1 .. '!items!zzz9JrtWjELdoAfK' @ 0 : 0; will stop at (end)
2026/01/09-16:51:28.868572 7f1c54bfb6c0 Manual compaction at level-1 from '!items!5dGXNiL3WN4cAk7X' @ 72057594037927935 : 1 .. '!items!zzz9JrtWjELdoAfK' @ 0 : 0; will stop at (end)
2026/05/02-23:32:07.882345 7fd754fed6c0 Recovering log #397
2026/05/02-23:32:07.973714 7fd754fed6c0 Delete type=3 #395
2026/05/02-23:32:07.973773 7fd754fed6c0 Delete type=0 #397
2026/05/02-23:32:54.296055 7fd7477fe6c0 Level-0 table #402: started
2026/05/02-23:32:54.296081 7fd7477fe6c0 Level-0 table #402: 0 bytes OK
2026/05/02-23:32:54.302413 7fd7477fe6c0 Delete type=0 #400
2026/05/02-23:32:54.309644 7fd7477fe6c0 Manual compaction at level-0 from '!items!5dGXNiL3WN4cAk7X' @ 72057594037927935 : 1 .. '!items!zzz9JrtWjELdoAfK' @ 0 : 0; will stop at (end)
Binary file not shown.
View File
View File
View File
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000303
MANIFEST-000403

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