Compare commits

...

37 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
uberwald 9836bd50e6 Resultat impari sur D20 dans messages et corrections diverses
Release Creation / build (release) Successful in 46s
2026-03-10 21:37:33 +01:00
uberwald 6b909b192b Resultat impari sur D20 dans messages et corrections diverses 2026-03-10 21:37:20 +01:00
uberwald e7a6c15bf7 Remove node_modules from repository
Release Creation / build (release) Successful in 56s
2026-01-09 17:25:00 +01:00
uberwald 1909ff443d Remove node_modules from repository 2026-01-09 17:24:36 +01:00
uberwald c72c9d8b02 Upgrade to auto-release 2026-01-09 17:23:02 +01:00
uberwald 536812e871 Upgrade to auto-release 2026-01-09 17:22:52 +01:00
uberwald 66fe1418f0 Migration datamodels ! 2026-01-09 17:11:12 +01:00
uberwald 901df5b395 Enhance CSS+fonts 2025-10-26 21:41:25 +01:00
uberwald f5d84832f3 CSS/Font enhancements 2025-10-26 15:08:45 +01:00
uberwald 216360e0d8 Correction sur usage BA sur un jet impair au D20 2025-06-24 22:11:57 +02:00
uberwald a0502dc467 Corrections cosmetiques v13 2025-06-03 13:49:09 +02:00
uberwald c8b94b74a1 Corrections cosmetiques v13 2025-06-03 13:48:23 +02:00
uberwald c9e4f125b4 Foundry v13 migration 2025-05-01 20:40:20 +02:00
uberwald e840b70ac9 Add UK/US translation 2024-09-10 13:17:45 +02:00
uberwald 03b8779862 Add UK/US translation 2024-09-10 07:20:55 +02:00
uberwald 580a2ccaba Corrections suite a migration Foundry v12 2024-05-01 09:46:37 +02:00
uberwald 699dba5511 Compatibilité v11/v12 2024-04-26 11:45:58 +02:00
uberwald aac552102a Add new automations 2024-04-05 22:33:36 +02:00
uberwald 4762dc33ac PNJ & Creatures 2024-03-03 08:20:30 +01:00
uberwald 0a030460e4 PNJ & Creatures 2024-03-02 23:58:27 +01:00
uberwald ed2dc58680 Enhance stats 2024-02-08 12:51:30 +01:00
uberwald 9c20f277ea Gestion plus fine ame+sante 2023-12-29 18:36:37 +01:00
uberwald 2a8617d781 Automatisations de combats, WIP 2023-12-24 12:38:02 +01:00
uberwald 91ad26730a Automatisations de combats, WIP 2023-12-22 09:30:26 +01:00
uberwald e1816b3dd7 CSS+Predilection+Niveau de competence 2023-12-21 19:21:06 +01:00
uberwald c48401a199 CSS+Predilection+Niveau de competence 2023-12-21 15:41:17 +01:00
uberwald f487908ecd Affichage des specialisations 2023-12-19 22:16:15 +01:00
580 changed files with 39598 additions and 3700 deletions
+112
View File
@@ -0,0 +1,112 @@
name: Release Creation
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo "💡 The ${{ gitea.repository }} repository will cloned to the runner."
#- uses: actions/checkout@v3
- uses: RouxAntoine/checkout@v3.5.4
# get part of the tag after the `v`
- name: Extract tag version number
id: get_version
uses: battila7/get-version-action@v2
# Substitute the Manifest and Download URLs in the system.json
- name: Substitute Manifest and Download Links For Versioned Ones
id: sub_manifest_link_version
uses: microsoft/variable-substitution@v1
with:
files: 'system.json'
env:
version: ${{steps.get_version.outputs.version-without-v}}
url: https://www.uberwald.me/gitea/${{gitea.repository}}
manifest: https://www.uberwald.me/gitea/public/fvtt-mournblade/releases/download/latest/system.json
download: https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{github.event.release.tag_name}}/fvtt-mournblade.zip
# Set up Node.js and build compendium packs from source
- name: Setup Node.js
uses: https://github.com/actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm install
- name: Build journal-aide compendium pack
run: |
rm -rf packs/journal-aide/journal-aide
./node_modules/.bin/fvtt package pack -n journal-aide --inputDirectory ./packs/src/journal-aide --outputDirectory ./packs/journal-aide
- name: Build pouvoirs-elementaires compendium pack
run: |
rm -rf packs/pouvoirs-elementaires/pouvoirs-elementaires
./node_modules/.bin/fvtt package pack -n pouvoirs-elementaires --inputDirectory ./packs/src/pouvoirs-elementaires --outputDirectory ./packs/pouvoirs-elementaires
- name: Build creatures-elementaires compendium pack
run: |
rm -rf packs/creatures-elementaires/creatures-elementaires
./node_modules/.bin/fvtt package pack -n creatures-elementaires --inputDirectory ./packs/src/creatures-elementaires --outputDirectory ./packs/creatures-elementaires
- name: Build automatons compendium pack
run: |
rm -rf packs/automatons/automatons
./node_modules/.bin/fvtt package pack -n automatons --inputDirectory ./packs/src/automatons --outputDirectory ./packs/automatons
- name: Build capacites-automaton compendium pack
run: |
rm -rf packs/capacites-automaton/capacites-automaton
./node_modules/.bin/fvtt package pack -n capacites-automaton --inputDirectory ./packs/src/capacites-automaton --outputDirectory ./packs/capacites-automaton
- name: Build traits-demoniaques compendium pack
run: |
rm -rf packs/traits-demoniaques/traits-demoniaques
./node_modules/.bin/fvtt package pack -n traits-demoniaques --inputDirectory ./packs/src/traits-demoniaques --outputDirectory ./packs/traits-demoniaques
- name: Build faiblesses-demoniaques compendium pack
run: |
rm -rf packs/faiblesses-demoniaques/faiblesses-demoniaques
./node_modules/.bin/fvtt package pack -n faiblesses-demoniaques --inputDirectory ./packs/src/faiblesses-demoniaques --outputDirectory ./packs/faiblesses-demoniaques
- name: Build demons-types compendium pack
run: |
rm -rf packs/demons-types/demons-types
./node_modules/.bin/fvtt package pack -n demons-types --inputDirectory ./packs/src/demons --outputDirectory ./packs/demons-types
# Create a zip file with all files required by the module to add to the release
- run: |
apt update -y
apt install -y zip
- run: zip -r ./fvtt-mournblade.zip system.json README.md LICENSE assets/ lang/ modules/ packs/ styles/ templates/ template.json
- name: setup go
uses: https://github.com/actions/setup-go@v4
with:
go-version: '>=1.20.1'
- name: Use Go Action
id: use-go-action
uses: https://gitea.com/actions/release-action@main
with:
files: |-
./fvtt-mournblade.zip
system.json
api_key: '${{secrets.ALLOW_PUSH_RELEASE}}'
- name: Publish to Foundry server
uses: https://github.com/djlechuck/foundryvtt-publish-package-action@v1
with:
token: ${{ secrets.FOUNDRYVTT_RELEASE_TOKEN }}
id: 'fvtt-mournblade'
version: ${{github.event.release.tag_name}}
manifest: 'https://www.uberwald.me/gitea/public/fvtt-mournblade/releases/download/latest/system.json'
notes: 'https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{github.event.release.tag_name}}/fvtt-mournblade.zip'
compatibility-minimum: '13'
compatibility-verified: '14'
+3
View File
@@ -1 +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

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

+35
View File
@@ -0,0 +1,35 @@
const gulp = require('gulp');
const less = require('gulp-less');
const sourcemaps = require('gulp-sourcemaps');
// Paths
const paths = {
styles: {
src: 'less/**/*.less',
dest: 'styles/'
}
};
// Compile LESS to CSS
function styles() {
return gulp.src('less/mournblade.less')
.pipe(sourcemaps.init())
.pipe(less())
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest(paths.styles.dest));
}
// Watch files
function watchFiles() {
gulp.watch(paths.styles.src, styles);
}
// Define complex tasks
const build = gulp.series(styles);
const watch = gulp.series(build, watchFiles);
// Export tasks
exports.styles = styles;
exports.build = build;
exports.watch = watch;
exports.default = build;
+323
View File
@@ -0,0 +1,323 @@
{
"Adresse": "Agility",
"Clairvoyance": "Clarity",
"MNBL.abilities": "Gifts/Pacts",
"MNBL.activatedrunes": "Activated Runes",
"MNBL.addpredilection": "Add a specialization",
"MNBL.aimingbonus": "Aiming Bonus",
"MNBL.alignement": "Alignment",
"MNBL.all": "All",
"MNBL.allegiance": "Allegiance",
"MNBL.applydamage": "Apply damage/bonus/penalty",
"MNBL.aspect": "Aspect",
"MNBL.Assaut": "Smite",
"MNBL.attack": "Attack",
"MNBL.attackcapacity": "Offensive Ability",
"MNBL.attackmountbonus": "Mounted attacker vs ground defender (+5)",
"MNBL.attacks": "Attacks",
"MNBL.attribut": "Attribute",
"MNBL.attributes": "Attributes",
"MNBL.automalus": "Auto Penalty",
"MNBL.base": "Base",
"MNBL.beastslords": "Beast Lords",
"MNBL.bio": "Bio & Notes",
"MNBL.bonus": "Bonus",
"MNBL.candoublebonusskill": "Bonuses can be doubled (cf. Profession)",
"MNBL.chaos": "Chaos",
"MNBL.chaotictraits": "Chaotic Traits",
"MNBL.charge": "Charge",
"MNBL.consumed": "Consumed",
"MNBL.contain": "Contain the opponent",
"MNBL.creatureresourcecost": "Resource Cost (creatures)",
"MNBL.current": "Current",
"MNBL.currentmax": "Current Max",
"MNBL.damage": "Damage",
"MNBL.damagebonus": "Damage Bonus",
"MNBL.defense": "Defense",
"MNBL.defensebonus": "Defense Bonus",
"MNBL.defensecapacity": "Defensive Ability",
"MNBL.dice": "Dice",
"MNBL.difficulty": "Difficulty",
"MNBL.dirtyattack": "Cheap Shot",
"MNBL.disadvantagepositions": "Disadvantageous positions (Max bonus +15)",
"MNBL.disarm": "Disarm",
"MNBL.doubleD20": "Double d20 (1 Shard Point)",
"MNBL.dramaticfailure": "Dramatic Failure",
"MNBL.oddresult": "Odd result — the die counts as 0",
"MNBL.duration": "Duration",
"MNBL.easy": "Easy (5)",
"MNBL.eclat": "Shard",
"MNBL.elementslords": "Elemental Lords",
"MNBL.equipmentactions": "Equipment/Actions",
"MNBL.equipments": "Equipments",
"MNBL.equipped": "Equipped",
"MNBL.exp": "Experience",
"MNBL.eyes": "Eyes",
"MNBL.failure": "Failure",
"MNBL.feint": "Feint",
"MNBL.flee": "Flee",
"MNBL.formula": "Formula",
"MNBL.genre": "Gender",
"MNBL.gifts": "Gifts",
"MNBL.goodadventure": "Good Adventure",
"MNBL.hair": "Hair",
"MNBL.hard": "Hard (15)",
"MNBL.hascover": "Cover",
"MNBL.hazardous": "Tricky (20)",
"MNBL.health": "Health",
"MNBL.healthmalus": "Health Penalty",
"MNBL.heavycover": "Almost complete (-10)",
"MNBL.heroicsuccess": "Heroic Success",
"MNBL.highlanguage": "High Melnibonéan",
"MNBL.ignorearmor": "Ignore Armor",
"MNBL.ignorehealthmalus": "Ignore Health Penalty",
"MNBL.ignoresoulmalus": "Ignore Soul Penalty",
"MNBL.immobilize": "Immobilize",
"MNBL.insane": "Ridiculous (25)",
"MNBL.isdefense": "Defensive",
"MNBL.knockout": "Knock Out",
"MNBL.law": "Law",
"MNBL.legacy": "Legacy",
"MNBL.lessthanshort": "Less than short (10)",
"MNBL.lethal": "Lethal",
"MNBL.level": "Level",
"MNBL.lightcover": "Buckler or light (-2)",
"MNBL.longmore": "Long and more (25)",
"MNBL.longrange": "Long Range",
"MNBL.malus": "Penalty",
"MNBL.margin": "Margin",
"MNBL.medium": "Average (10)",
"MNBL.mediumcover": "Pavise or half (-5)",
"MNBL.mediummore": "Medium and more (20)",
"MNBL.mediumrange": "Medium Range",
"MNBL.meleethrowweapon": "Melee and Throwing Weapon",
"MNBL.meleeweapon": "Melee Weapon",
"MNBL.mode": "Mode",
"MNBL.modifier": "Modifiers",
"MNBL.modifiertype": "Modifier Type",
"MNBL.mounted": "Mounted",
"MNBL.nextactionmalus": "Penalty for next action",
"MNBL.nextattackbonus": "Bonus for next attack",
"MNBL.none": "None",
"MNBL.noneunknwon": "None/Unknown",
"MNBL.nonlethal": "Non-Lethal",
"MNBL.nonlethaldamage": "Non-Lethal Damage",
"MNBL.notarget": "No designated target",
"MNBL.origin": "Origin",
"MNBL.pacts": "Pacts",
"MNBL.points": "Points",
"MNBL.preciseattack": "Precision Attack",
"MNBL.predilections": "Specializations",
"MNBL.preferredhand": "Preferred Hand",
"MNBL.prerequisites": "Prerequisites",
"MNBL.price": "Price",
"MNBL.profession": "Profession",
"MNBL.pronounced": "Spoken",
"MNBL.pronouncedrune": "Spoken Rune",
"MNBL.pronouncerune": "Speak the rune",
"MNBL.protections": "Protections",
"MNBL.puremadness": "Insane (30)",
"MNBL.quantity": "Quantity",
"MNBL.range": "Range",
"MNBL.rarity": "Rarity",
"MNBL.registeredmodifiers": "Registered Modifiers",
"MNBL.reloadduration": "Reload Time",
"MNBL.ressources": "Resources",
"MNBL.roll": "Roll",
"MNBL.runes": "Runes",
"MNBL.runningtarget": "Running target (-5/-10 depending on range)",
"MNBL.shootmodifier": "Shooting Modifiers",
"MNBL.shootweapon": "Shoot Weapon",
"MNBL.shortmore": "Short and more (10)",
"MNBL.shortrange": "Short Range",
"MNBL.size": "Size",
"MNBL.skill": "Skill",
"MNBL.skills": "Skills",
"MNBL.smallroomtarget": "Target in confined space (+5)",
"MNBL.soul": "Soul",
"MNBL.soulmalus": "Soul Penalty",
"MNBL.soulmultiplier": "Soul Multiplier",
"MNBL.soulpoints": "Soul Points",
"MNBL.specialactions": "Special Actions",
"MNBL.specialweapon": "Special (ability/gift)",
"MNBL.speciestrait": "Species Trait",
"MNBL.speed": "Speed",
"MNBL.success": "Success",
"MNBL.target": "Target",
"MNBL.targetbelow": "Target below (+5)",
"MNBL.targetcantmove": "Target immobilized (+5)",
"MNBL.targetdefense": "Opponent's Defense",
"MNBL.targetground": "Target on the ground (+5)",
"MNBL.targetseeshoot": "Target is aware of the shot",
"MNBL.tendancies": "Tendencies",
"MNBL.throwweapon": "Throwing Weapon",
"MNBL.totalprotection": "Total Protection",
"MNBL.traced": "Written",
"MNBL.tracedrune": "Written Rune",
"MNBL.tracerune": "Write the rune",
"MNBL.treasuremoney": "Treasures and Money",
"MNBL.twohands": "Two-handed",
"MNBL.type": "Type",
"MNBL.unarmedtarget": "Unarmed target (+5)",
"MNBL.unit": "Unit",
"MNBL.use": "Use",
"MNBL.usedpredilection": "Used Predilection",
"MNBL.value": "Value",
"MNBL.weapon": "Weapon",
"MNBL.weaponbonusattack": "Handling Bonus (offensive)",
"MNBL.weaponbonusdefense": "Handling Bonus (defensive)",
"MNBL.weapons": "Weapons",
"MNBL.weaponscapacities": "Weapons/Abilities",
"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"
}
},
"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)"
}
+328 -22
View File
@@ -1,27 +1,333 @@
{
"ACTOR": {
"TypePersonnage": "Personnage",
"TypePNJ": "PNJ"
"TYPES": {
"Actor": {
"personnage": "Personnage",
"creature": "Créature"
},
"Item": {
"arme": "Arme",
"competence": "Compétence",
"protection": "Protection",
"pacte": "Pacte",
"traitchaotique": "Trait Chaotique",
"monnaie": "Monnaie",
"don": "Don",
"tendance": "Tendance",
"rune": "Rune",
"equipement": "Equipement",
"capacite": "Capacité",
"origine": "Origine",
"heritage": "Héritage",
"metier": "Métier",
"runeeffect": "Effet de Rune",
"bouclier": "Bouclier",
"modifier": "Modificateur",
"traitespece": "Trait d'Espèce",
"potion": "Potion"
}
},
"Adresse": "Adresse",
"Puissance": "Puissance",
"Clairvoyance": "Clairvoyance",
"Présence": "Présence",
"Trempe": "Trempe",
"MNBL.assaut": "Assaut",
"MNBL.preciseattack": "Attaque Précise",
"MNBL.feint": "Feinte",
"MNBL.dirtyattack": "Coup Bas",
"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)",
"MNBL.heavycover": "Quasi complet (-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)",
"MNBL.hard": "Ardue (15)",
"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.base": "Base",
"MNBL.current": "Actuelle",
"MNBL.alignement": "Alignement",
"MNBL.eclat": "Eclat",
"MNBL.exp": "Expérience",
"MNBL.attributes": "Attributs",
"MNBL.skills": "Compétences",
"MNBL.abilities": "Dons/Pactes",
"MNBL.equipmentactions": "Equipements/Actions",
"MNBL.bio": "Bio&Notes",
"MNBL.health": "Santé",
"MNBL.bonus": "Bonus",
"MNBL.malus": "Malus",
"MNBL.nonlethal": "Non Létaux",
"MNBL.lethal": "Létaux",
"MNBL.automalus": "Malus Auto",
"MNBL.soul": "Ame",
"MNBL.currentmax": "Max Actuel",
"MNBL.consumed": "Consommé",
"MNBL.damagebonus": "B. Dégats",
"MNBL.speed": "Vitesse",
"MNBL.defense": "Défense",
"MNBL.totalprotection": "Protection Totale",
"MNBL.modifier": "Modificateurs",
"MNBL.type": "Type",
"MNBL.value": "Valeur",
"MNBL.gifts": "Dons",
"MNBL.allegiance": "Allégeance",
"MNBL.pacts": "Pactes",
"MNBL.runes": "Runes",
"MNBL.highlanguage": "Haut Parler",
"MNBL.difficulty": "Difficulté",
"MNBL.tendancies": "Tendances",
"MNBL.chaotictraits": "Traits Chaotique",
"MNBL.activatedrunes": "Runes actives",
"MNBL.mode": "Mode",
"MNBL.duration": "Durée",
"MNBL.treasuremoney": "Richesses et Argent",
"MNBL.quantity": "Quantité",
"MNBL.unit": "Unité",
"MNBL.specialactions": "Actions spéciales",
"MNBL.knockout": "Assomer",
"MNBL.flee": "Fuir",
"MNBL.immobilize": "Immobiliser",
"MNBL.mounted": "Monté",
"MNBL.weapons": "Armes",
"MNBL.attack": "Attaque",
"MNBL.damage": "Dégats",
"MNBL.protections": "Protections",
"MNBL.equipments": "Equipements",
"MNBL.equipment": "Equipement",
"MNBL.origin": "Origine",
"MNBL.legacy": "Héritage",
"MNBL.profession": "Métier",
"MNBL.genre": "Genre",
"MNBL.size": "Taille",
"MNBL.hair": "Cheveux",
"MNBL.eyes": "Yeux",
"MNBL.preferredhand": "Main Préférée",
"MNBL.weight": "Poids",
"MNBL.soulmultiplier": "Multiplicateur d'âme",
"MNBL.ignorehealthmalus": "Ignore le malus de santé",
"MNBL.ignoresoulmalus": "Ignore le malus d'âme",
"MNBL.weapon": "Arme",
"MNBL.nextattackbonus": "Bonus pour prochaine attaque",
"MNBL.nextactionmalus": "Malus au défenseur pour prochaine action",
"MNBL.applydamage": "Appliquer les dégats/bonus/malus",
"MNBL.attribut": "Attribut",
"MNBL.skill": "Compétence",
"MNBL.target": "Cible",
"MNBL.usedpredilection": "Prédilection utilisée",
"MNBL.soulpoints": "Points d'âme",
"MNBL.formula": "Formule",
"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",
"MNBL.aimingbonus": "Bonus de visée",
"MNBL.targetseeshoot": "La cible est consciente du tir",
"MNBL.notarget": "Pas de cible désignée",
"MNBL.runningtarget": "La cible court (-5/-10 selon portée)",
"MNBL.hascover": "Couvert",
"MNBL.range": "Portée",
"MNBL.disadvantagepositions": "Positions désavantageuses (Bonus max +15)",
"MNBL.targetground": "Cible au sol (+5)",
"MNBL.unarmedtarget": "Cible désarmée (+5)",
"MNBL.smallroomtarget": "Cible en espace restreint (+5)",
"MNBL.targetcantmove": "Cible immobilisée (+5)",
"MNBL.targetbelow": "Cible surplombée (+5)",
"MNBL.healthmalus": "Malus de santé",
"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é",
"MNBL.rarity": "Rareté",
"MNBL.price": "Prix",
"MNBL.modifiertype": "Type de modificateur",
"MNBL.prerequisites": "Prérequis",
"MNBL.predilections": "Prédilections",
"MNBL.candoublebonusskill": "Les bonus de Bonne Aventure et d'Eclat peuvent être doublés (cf. Métier)",
"MNBL.addpredilection": "Ajouter une prédilection",
"MNBL.defensebonus": "Bonus de défense",
"MNBL.nonlethaldamage": "Dégâts non létaux",
"MNBL.weapontype": "Type d'arme",
"MNBL.weaponbonusattack": "Bonus de maniement (offensif)",
"MNBL.weaponbonusdefense": "Bonus de maniement (défensif)",
"MNBL.isdefense": "Défensive",
"MNBL.twohands": "A deux mains",
"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.reloadduration": "Temps de rechargement",
"MNBL.attacks": "Attaques",
"MNBL.ressources": "Ressources",
"MNBL.weaponscapacities": "Armes/Capacités",
"MNBL.use": "Utiliser",
"MNBL.speciestrait": "Trait d'espèce",
"MNBL.attribute": "Attribut",
"MNBL.Protections": "Protections",
"MNBL.rune": "Rune",
"MNBL.total": "Total",
"MNBL.description": "Description",
"MNBL.details": "Détails",
"MNBL.sacrifice": "Sacrifice",
"MNBL.identity": "Identité",
"MNBL.gmtools": "Outils MJ",
"MNBL.potion": "Potion",
"MNBL.potionRune": "Rune",
"MNBL.potionSeuil": "Seuil de la Rune",
"MNBL.potionPointsAme": "Points d'Âme investis",
"MNBL.potionForme": "Forme",
"MNBL.potionStatut": "Statut",
"MNBL.potionVirulence": "Virulence",
"MNBL.potionDuree": "Durée d'effet",
"MNBL.potionConservation": "Conservation",
"MNBL.potionTemps": "Temps de préparation",
"MNBL.potionEffetCuratif": "Effet curatif",
"MNBL.potionEffetLetal": "Effet létal",
"MNBL.potionEffetSecondaire": "Effets secondaires",
"MNBL.potionDescription": "Description",
"MNBL.potionLiquide": "Liquide",
"MNBL.potionOnguent": "Onguent",
"MNBL.potionCachets": "Cachets",
"MNBL.potionPilules": "Pilules",
"MNBL.potionInconnue": "Inconnue",
"MNBL.potionEfficace": "Efficace",
"MNBL.potionHeroique": "Héroïque",
"MNBL.potionInefficace": "Inefficace",
"MNBL.potionPoison": "Poison",
"MNBL.potions": "Potions",
"MNBL.potionStatut": "Statut",
"MNBL.potionDuree": "Durée",
"MNBL.preparePotion": "Préparer une Potion",
"MNBL.capacites": "Capacités / Pouvoirs",
"MNBL.capacite": "Capacité",
"MNBL.typeCapacite": "Type de capacité",
"MNBL.typeCapaciteElue": "Pouvoir Élue",
"MNBL.typeCapaciteElementaire": "Pouvoir Élémentaire",
"MNBL.typeCapaciteDemoniaque": "Pouvoir Démoniaque",
"MNBL.typeCapaciteAutomaton": "Capacité Automaton",
"MNBL.typeCapaciteCreature": "Pouvoir Créature",
"MNBL.typeCapaciteFaiblesseDemoniaque": "Faiblesse Démoniaque",
"MNBL.creatureTypeCreature": "Créature",
"MNBL.creatureTypeDemon": "Démon",
"MNBL.creatureTypeElementaire": "Élémentaire",
"MNBL.elementTypeAir": "Air",
"MNBL.elementTypeTerre": "Terre",
"MNBL.elementTypeFeu": "Feu",
"MNBL.elementTypeEau": "Eau",
"MNBL.creatureType": "Type de créature",
"MNBL.elementType": "Élément",
"MNBL.demonType": "Type de Démon",
"MNBL.demonPuissance": "Puissance",
"MNBL.demonTypeNone": "— Non défini —",
"MNBL.demonTypeCombat": "Démon de Combat",
"MNBL.demonTypeDesir": "Démon du Désir",
"MNBL.demonTypeSavoir": "Démon du Savoir",
"MNBL.demonTypeProtection": "Démon de Protection",
"MNBL.demonTypeVoyage": "Démon du Voyage",
"MNBL.demonPuissanceNone": "— Non défini —",
"MNBL.demonPuissanceMineur": "Mineur",
"MNBL.demonPuissanceMedian": "Médian",
"MNBL.demonPuissanceMajeur": "Majeur",
"MNBL.creatureTypeAutomaton": "Automaton",
"MNBL.automatonType": "Type d'Automaton",
"MNBL.automatonPuissance": "Puissance",
"MNBL.automatonTypeNone": "— Non défini —",
"MNBL.automatonTypeCombat": "Combat",
"MNBL.automatonTypeVoyage": "Voyage",
"MNBL.automatonTypePerception": "Perception",
"MNBL.automatonTypeRestauration": "Restauration",
"MNBL.automatonTypeReparateur": "Réparateur",
"MNBL.automatonPuissanceNone": "— Non défini —",
"MNBL.automatonPuissanceMineur": "Mineur",
"MNBL.automatonPuissanceMedian": "Médian",
"MNBL.automatonPuissanceMajeur": "Majeur",
"MNBL.automatonVoyageType": "Type de voyage",
"MNBL.automatonVoyageTypeNone": "— Non défini —",
"MNBL.automatonVoyageTypeTerrestre": "Terrestre",
"MNBL.automatonVoyageTypeAquatique": "Aquatique",
"MNBL.automatonVoyageTypeAerien": "Aérien",
"MNBL.automatonVoyageTypeExtradimensionnel": "Extra-dimensionnel",
"MNBL.invoquerElementaire": "Invoquer un Élémentaire",
"MNBL.bannirElementaire": "Bannir",
"MNBL.invocationsActives": "Invocations actives",
"MNBL.invocationElement": "Élément",
"MNBL.invocationTier": "Puissance",
"MNBL.invocationSeuil": "Seuil",
"MNBL.invocationAmeExtra": "Points d'Âme supplémentaires",
"MNBL.invocationAmeTotal": "Coût total en Âme",
"MNBL.invocationTemps": "Temps de concentration",
"MNBL.invocationBonusPacte": "Bonus Pacte élémentaire",
"MNBL.invocationHautParler": "Savoir : Haut-Parler",
"MNBL.invocationSeigneursElem": "Savoir : Seigneurs Élémentaires",
"MNBL.invocationResultSucces": "L'Élémentaire est invoqué !",
"MNBL.invocationResultHeroique": "Réussite héroïque ! Vous pouvez majorer gratuitement de +2 un attribut de l'Élémentaire.",
"MNBL.invocationResultEchec": "L'invocation a échoué. Vous perdez la moitié des points d'Âme dépensés.",
"MNBL.invocationResultDramatique": "Échec dramatique ! L'invocation a échoué et vous perdez tous les points d'Âme investis. Une catastrophe naturelle se déclenche !",
"MNBL.invocationTierMineur": "Mineur (Seuil 15)",
"MNBL.invocationTierMedian": "Médian (Seuil 20)",
"MNBL.invocationTierMajeur": "Majeur (Seuil 25)",
"MNBL.invocationConcentrationMineur": "1 tour",
"MNBL.invocationConcentrationMedian": "1 minute",
"MNBL.invocationConcentrationMajeur": "1 heure",
"MNBL.invocationAmeBloque": "Âme bloquée",
"ITEM": {
"TypeArme": "Arme",
"TypeCompetence": "Compétence",
"TypeProtection": "Protection",
"TypePacte": "Pacte",
"TypeTraitchaotique": "Trait Chaotique",
"TypeMonnaie": "Monnaie",
"TypeDon": "Don",
"TypeTendance": "Tendance",
"TypeRune": "Rune",
"TypeEquipement": "Equipement",
"TypeCapacite": "Capacité",
"TypeOrigine": "Origine",
"TypeHeritage": "Héritage",
"TypeMetier": "Métier",
"TypeRuneeffect": "Effet de Rune",
"TypeBouclier": "Bouclier"
}
"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)"
}
+950
View File
@@ -0,0 +1,950 @@
/* ==================== Actor Sheet Styles ==================== */
// NOTE: Ce fichier surcharge certaines règles de simple-converted.less
// Les sélecteurs .fvtt-mournblade.actor sont plus spécifiques et prennent priorité
.fvtt-mournblade.actor {
// Background pour toute la fiche d'acteur
background: url("../assets/ui/pc_sheet_bg.webp") repeat;
// AppV2 - Structure flex pour permettre le scroll
.window-content {
height: 100%;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
// Main form structure
form,
.sheet-form-layout {
height: 100%;
background: url("../assets/ui/pc_sheet_bg.webp") repeat-y;
color: black;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
padding: 0;
margin: 0;
// La section racine du template (if nested inside form, or simple direct children handling)
> section {
height: 100%;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
}
}
// SURCHARGE: simple-converted.less définit flex: 0 0 210px
.sheet-header {
background: url("../assets/ui/pc_sheet_bg.webp") repeat;
padding: 0.5rem;
margin: 0;
flex: 0 0 auto !important; // Override simple-converted
overflow: visible !important; // Override simple-converted
.background-sheet-header {
background: transparent;
}
.header-main-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.header-stats-grid {
display: grid;
grid-template-columns: 2fr 1fr 1fr;
gap: 0.75rem;
padding: 0.5rem;
background: rgba(0, 0, 0, 0.15);
border: 2px solid rgba(139, 69, 19, 0.5);
border-radius: 4px;
}
.stat-group {
display: flex;
flex-direction: column;
gap: 0.3rem;
padding: 0.35rem 0.6rem;
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(139, 69, 19, 0.4);
border-radius: 3px;
&.stat-group-health {
border-left: 3px solid rgba(200, 0, 0, 0.6);
background: rgba(40, 0, 0, 0.15);
}
}
.stat-title {
font-size: 0.75rem;
font-weight: bold;
color: #f5e6d3;
margin: 0;
padding: 0 0 0.3rem 0;
border-bottom: 1px solid rgba(139, 69, 19, 0.5);
text-transform: uppercase;
letter-spacing: 0.5px;
font-family: "CentaurMT", serif;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
}
.stat-values {
display: flex;
flex-direction: row;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: center;
}
.stat-item {
display: flex;
flex-direction: column;
gap: 0.2rem;
align-items: center;
flex: 1;
min-width: 0;
}
.stat-label {
font-size: 0.7rem;
color: #d4c5b0;
margin: 0;
font-weight: 600;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.7);
}
.stat-value {
font-size: 0.85rem;
font-weight: bold;
color: #fff;
padding: 0.25rem 0.5rem;
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(139, 69, 19, 0.6);
border-radius: 3px;
min-width: 2.5rem;
text-align: center;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.9);
width: 100%;
}
.stat-input {
font-size: 0.8rem;
font-weight: 600;
padding: 0.25rem 0.4rem;
background: rgba(40, 30, 20, 0.7);
border: 1px solid rgba(139, 69, 19, 0.7);
color: #fff;
border-radius: 3px;
text-align: center;
min-width: 2.5rem;
max-width: 100%;
width: 100%;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.9);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3);
&.stat-input-single {
min-width: 3.5rem;
}
&.stat-input-damage {
border-color: rgba(200, 0, 0, 0.8);
background: rgba(80, 0, 0, 0.5);
}
&:hover {
background: rgba(60, 45, 30, 0.8);
border-color: rgba(255, 102, 0, 0.6);
}
&:focus {
outline: none;
border-color: rgba(255, 102, 0, 1);
box-shadow: 0 0 6px rgba(255, 102, 0, 0.5), inset 0 1px 3px rgba(0, 0, 0, 0.3);
background: rgba(70, 50, 35, 0.9);
}
}
.header-fields {
h4.item-name-label.competence-name {
font-size: 0.75rem;
padding-top: 3px;
}
.item-name-label.competence-name {
font-size: 0.75rem;
}
label.item-name-label.competence-name {
font-size: 0.7rem;
}
.item-field-label-short {
font-size: 0.8rem;
}
.status-small-label {
font-size: 0.8rem;
}
}
}
// Sheet navigation tabs
// SURCHARGE: simple-converted.less définit flex: 0
.sheet-tabs {
margin: 0;
padding: 0 0.5rem;
flex: 0 0 auto !important; // Override simple-converted
}
// Sheet body - section scrollable
// SURCHARGE CRITIQUE: simple-converted.less définit height: auto sur .sheet-body et .tab
// Ces surcharges permettent le scroll vertical
.sheet-body {
margin: 0;
padding: 0.5rem;
flex: 1 !important; // Override simple-converted
min-height: 0 !important; // Critique pour le scroll
overflow-y: auto !important; // Override simple-converted
.tab {
padding: 0;
height: auto !important; // Override simple-converted qui met height: auto
&:not(.active) {
display: none;
}
// Assurer que les grilles peuvent scroller
.grid, .grid-2col {
height: auto;
overflow: visible;
}
.sheet-box {
height: auto;
}
}
// Listes compactes dans les sections
.compact-list {
list-style: none;
margin: 0;
padding: 0;
li.item {
padding: 0.2rem 0.4rem;
margin-bottom: 0.15rem;
background: rgba(0, 0, 0, 0.1);
border: 1px solid rgba(139, 69, 19, 0.3);
border-radius: 3px;
&:hover {
background: rgba(0, 0, 0, 0.15);
}
&.items-title-bg {
background: rgba(0, 0, 0, 0.3);
border-color: rgba(139, 69, 19, 0.5);
font-weight: 600;
margin-bottom: 0.3rem;
.item-name-label-header {
color: #f5e6d3;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.7);
}
}
}
}
// Contrôles d'items (edit, delete, equip, etc.)
.item-controls {
display: flex;
align-items: center;
gap: 0.3rem;
justify-content: flex-end;
.item-control {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
color: #5a3a1a;
text-decoration: none;
cursor: pointer;
border-radius: 3px;
transition: all 0.2s;
&:hover {
color: #2a1a0a;
background: rgba(139, 69, 19, 0.2);
transform: scale(1.1);
}
i {
font-size: 0.85rem;
}
&.item-edit {
color: #4a7c59;
&:hover {
color: #2d5a3a;
background: rgba(74, 124, 89, 0.2);
}
}
&.item-delete {
color: #a04040;
&:hover {
color: #802020;
background: rgba(160, 64, 64, 0.2);
}
}
&.item-equip {
color: #6b5b3a;
&:hover {
color: #4a3a1a;
background: rgba(107, 91, 58, 0.2);
}
i.fa-circle {
color: #4a7c59;
}
i.fa-genderless {
color: #8a6a4a;
}
}
&.item-add {
color: #4a7c59;
&:hover {
color: #2d5a3a;
background: rgba(74, 124, 89, 0.2);
}
}
}
}
.item-controls-fixed {
min-width: 5rem;
max-width: 5rem;
}
// Couleurs pour les labels et textes dans les onglets
h4, h3, label, span.item-name-label, span.competence-name,
.label-name, .generic-label, .item-field-label-short,
.item-field-label-medium, .item-field-label-long,
.short-label, .items-title-text {
color: #3a2a1a !important;
text-shadow: 0px 0px 1px rgba(255, 255, 255, 0.3);
}
// Inputs dans le corps
input[type="text"], input[type="number"], select {
color: #2a1a0a;
background: rgba(255, 250, 240, 0.8);
border: 1px solid rgba(139, 69, 19, 0.5);
height: 24px;
padding: 0.15rem 0.3rem;
line-height: 1.2;
&:focus {
background: rgba(255, 255, 245, 0.95);
border-color: rgba(139, 69, 19, 0.8);
}
}
// Titres de sections
h3, h4 {
font-weight: bold;
color: #2a1a0a !important;
}
// Titres de sections Santé, Âme, Combat
h4.item-name-label.competence-name {
font-size: 0.85rem;
margin-top: 0.2rem;
margin-bottom: 0.3rem;
padding-top: 0;
}
// Section grids pour Santé, Âme, Combat
.section-grid {
display: flex;
flex-direction: column;
gap: 0.2rem;
padding: 0.3rem 0.4rem;
margin-bottom: 0.3rem;
background: rgba(0, 0, 0, 0.15);
border: 2px solid rgba(139, 69, 19, 0.5);
border-radius: 4px;
.section-title {
font-size: 0.85rem;
font-weight: bold;
color: #f5e6d3;
text-transform: uppercase;
letter-spacing: 0.5px;
margin: 0 0 0.2rem 0;
padding: 0.2rem 0.4rem;
background: rgba(0, 0, 0, 0.3);
border-left: 3px solid rgba(139, 69, 19, 0.8);
border-radius: 2px;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.9);
}
.grid-row {
display: grid;
grid-template-columns: 1fr 1fr;
align-items: center;
gap: 0.4rem;
padding: 0.25rem 0.4rem;
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(139, 69, 19, 0.4);
border-radius: 3px;
input {
width: 60px;
min-width: 60px;
}
&.attr-row {
grid-template-columns: 40px 1fr auto;
gap: 0.5rem;
.item-name-img {
width: 32px;
height: 32px;
border-radius: 3px;
border: 1px solid rgba(139, 69, 19, 0.5);
}
.label-name {
display: flex;
align-items: center;
a {
color: #f5e6d3;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.7);
font-weight: 600;
cursor: pointer;
&:hover {
color: #fff;
text-shadow: 0 0 4px rgba(255, 200, 100, 0.8);
}
}
}
select {
width: 80px;
}
}
.label-name {
font-weight: 600;
font-size: 0.9rem;
color: #d4c5b0;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.7);
}
.stat-label {
font-weight: 600;
font-size: 0.9rem;
color: #d4c5b0;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.7);
}
.value-display {
text-align: center;
font-weight: bold;
font-size: 1rem;
color: #fff;
background: rgba(0, 0, 0, 0.4);
padding: 0.2rem 0.4rem;
border-radius: 3px;
border: 1px solid rgba(139, 69, 19, 0.6);
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.9);
}
.malus-value {
text-align: center;
font-weight: bold;
font-size: 1rem;
color: #ff9999;
background: rgba(0, 0, 0, 0.4);
padding: 0.2rem 0.4rem;
border-radius: 3px;
border: 1px solid rgba(200, 50, 50, 0.6);
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.9);
}
&.damage-row {
grid-template-columns: 120px 1fr;
.damage-label {
font-size: 0.95rem;
font-weight: 600;
}
.damage-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
input {
width: 60px;
min-width: 60px;
flex-shrink: 0;
text-align: center;
font-size: 1.1rem;
font-weight: bold;
}
.plus-minus-button {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
font-weight: bold;
border-radius: 4px;
border: 2px solid rgba(139, 69, 19, 0.5);
background: linear-gradient(to bottom, rgba(210, 180, 140, 0.9), rgba(180, 150, 110, 0.9));
color: #2a1a0a;
cursor: pointer;
transition: all 0.15s;
&:hover {
background: linear-gradient(to bottom, rgba(230, 200, 160, 0.95), rgba(200, 170, 130, 0.95));
border-color: rgba(139, 69, 19, 0.8);
transform: scale(1.08);
}
&:active {
transform: scale(0.95);
}
}
}
}
&.soul-consumed-row {
grid-template-columns: 120px 1fr;
}
&.soul-malus-row {
grid-template-columns: 90px 1fr;
gap: 0.4rem;
.label-name {
font-size: 0.85rem;
}
.malus-value {
font-size: 0.9rem;
padding: 0.2rem 0.3rem;
}
}
&.malus-row {
grid-template-columns: 55px 60px 75px 50px;
gap: 0.3rem;
.label-name {
font-size: 0.85rem;
}
input {
width: 100%;
min-width: unset;
}
.malus-value {
font-size: 0.9rem;
padding: 0.2rem 0.3rem;
}
}
}
&.combat-grid {
.grid-row.combat-stat {
grid-template-columns: 85px 50px 60px 55px;
gap: 0.4rem;
.stat-label {
font-size: 0.85rem;
}
.stat-base {
text-align: center;
font-weight: bold;
font-size: 0.85rem;
color: #fff;
background: rgba(0, 0, 0, 0.4);
padding: 0.2rem 0.3rem;
border-radius: 3px;
border: 1px solid rgba(139, 69, 19, 0.6);
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.9);
}
input {
width: 50px;
min-width: 50px;
text-align: center;
}
.stat-total {
text-align: center;
font-weight: bold;
font-size: 0.85rem;
color: #fff;
background: rgba(0, 0, 0, 0.4);
padding: 0.2rem 0.3rem;
border-radius: 3px;
border: 1px solid rgba(139, 69, 19, 0.6);
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.9);
}
}
.grid-row.protection-row {
grid-template-columns: 1fr 1fr;
.protection-value {
text-align: center;
font-weight: bold;
font-size: 1.2rem;
color: #fff;
background: rgba(0, 0, 0, 0.4);
padding: 0.25rem 0.4rem;
border-radius: 3px;
border: 1px solid rgba(139, 69, 19, 0.6);
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.9);
}
}
}
}
// Boutons d'actions spéciales
.action-buttons {
display: flex;
flex-direction: column;
gap: 0.3rem;
padding: 0.3rem 0;
button {
margin: 0;
}
}
.action-buttons-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
padding: 0.3rem 0;
button {
margin: 0;
flex: 1;
}
.mounted-checkbox {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.3rem 0.6rem;
white-space: nowrap;
label {
margin: 0;
font-size: 0.9rem;
}
input[type="checkbox"] {
margin: 0;
cursor: pointer;
}
}
}
}
}
/* ==================== Creature Sheet Specific Styles ==================== */
.fvtt-mournblade.actor.creature-sheet {
// Variant background pour creatures - teinte légèrement différente
.background-sheet-header-creature {
background: linear-gradient(135deg, rgba(0, 60, 0, 0.15) 0%, rgba(20, 80, 20, 0.1) 100%);
border: 2px solid rgba(34, 139, 34, 0.5);
border-radius: 4px;
padding: 0;
}
// Légère teinte verte pour les sections
.sheet-box {
&.color-bg-archetype {
background: linear-gradient(135deg, rgba(0, 40, 0, 0.08) 0%, rgba(20, 60, 20, 0.05) 100%);
}
}
// Header simplifié pour creatures
.sheet-header {
flex: 0 0 auto !important;
padding: 0;
}
.profile-img {
width: 80px;
height: 80px;
border-radius: 8px;
border: 2px solid rgba(34, 139, 34, 0.6);
object-fit: cover;
cursor: pointer;
flex-shrink: 0;
}
// Tabs avec teinte verte
nav.tabs {
.item.active {
border-bottom-color: rgba(34, 139, 34, 0.8);
color: #228b22;
}
}
// Section titles avec teinte verte
.section-title {
color: #1a5a1a;
border-bottom-color: rgba(34, 139, 34, 0.3);
}
// Items headers
.items-title-bg {
background: linear-gradient(135deg, rgba(0, 60, 0, 0.15) 0%, rgba(20, 80, 20, 0.1) 100%);
border-bottom: 1px solid rgba(34, 139, 34, 0.3);
}
}
// PNJ Sheet - Orange/Copper theme for visual distinction
.fvtt-mournblade.actor.pnj-sheet {
// Variant background pour PNJs - teinte orange/cuivre
.background-sheet-header-creature {
background: linear-gradient(135deg, rgba(80, 40, 0, 0.15) 0%, rgba(100, 50, 0, 0.1) 100%);
border: 2px solid rgba(205, 127, 50, 0.5);
border-radius: 4px;
padding: 0;
}
// Légère teinte orange/cuivre pour les sections
.sheet-box {
&.color-bg-archetype {
background: linear-gradient(135deg, rgba(60, 30, 0, 0.08) 0%, rgba(80, 40, 0, 0.05) 100%);
}
}
// Header simplifié pour PNJs
.sheet-header {
flex: 0 0 auto !important;
padding: 0;
}
.profile-img {
width: 80px;
height: 80px;
border-radius: 8px;
border: 2px solid rgba(205, 127, 50, 0.6);
object-fit: cover;
cursor: pointer;
flex-shrink: 0;
}
// Tabs avec teinte orange/cuivre
nav.tabs {
.item.active {
border-bottom-color: rgba(205, 127, 50, 0.8);
color: #cd7f32;
}
}
// Section titles avec teinte orange/cuivre
.section-title {
color: #b8734d;
border-bottom-color: rgba(205, 127, 50, 0.3);
}
// Items headers
.items-title-bg {
background: linear-gradient(135deg, rgba(80, 40, 0, 0.15) 0%, rgba(100, 50, 0, 0.1) 100%);
border-bottom: 1px solid rgba(205, 127, 50, 0.3);
}
}
// GM Tools section - labels plus larges
.gm-tools-section {
.grid {
.item-list .flexrow.item {
.label-name {
flex: 1;
min-width: 12rem;
}
}
}
}
// Alignment group in header
.stat-group-alignment {
.stat-values.alignment-values {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.alignment-row {
display: flex;
align-items: center;
gap: 0.3rem;
.alignment-label {
font-weight: bold;
min-width: 4rem;
font-size: 0.85rem;
}
.alignment-inputs {
display: flex;
align-items: center;
gap: 0.2rem;
flex: 1;
}
.stat-label-mini {
font-size: 0.7rem;
min-width: 2.5rem;
}
.stat-input-mini {
width: 2.5rem;
height: 1.5rem;
padding: 0.1rem 0.2rem;
font-size: 0.8rem;
}
select.stat-input-mini {
width: 3rem;
}
}
.alignment-info {
display: flex;
justify-content: space-around;
margin-top: 0.2rem;
padding-top: 0.2rem;
border-top: 1px solid rgba(139, 69, 19, 0.3);
.info-label {
font-size: 0.75rem;
color: #f5e6d3;
strong {
color: #ffd700;
}
}
}
}
/* ==================== Sortilège Launch Button ==================== */
.fvtt-mournblade.sheet {
.header-actions {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
padding: 4px 0;
.chat-card-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 12px;
background: linear-gradient(135deg, #6b1a1a 0%, #8b2222 50%, #6b1a1a 100%);
border: 1px solid #c0392b;
border-radius: 4px;
color: #f5e6d3;
font-family: CentaurMT, serif;
font-size: 0.9rem;
cursor: pointer;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.1);
transition: all 0.2s ease;
white-space: nowrap;
i { color: #d4af37; }
&:hover {
background: linear-gradient(135deg, #8b2222 0%, #a52a2a 50%, #8b2222 100%);
border-color: #d4af37;
color: #fff;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.15);
i { color: #ffd700; }
}
&:active {
background: linear-gradient(135deg, #5a1010 0%, #6b1a1a 50%, #5a1010 100%);
transform: translateY(1px);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
}
}
}
}
// Chat potion result — rune mini image and haut-parler styling
.mournblade-chat-result {
.detail-row {
.rune-mini-img {
width: 18px;
height: 18px;
border-radius: 2px;
object-fit: cover;
flex-shrink: 0;
border: 1px solid rgba(139, 69, 19, 0.4);
vertical-align: middle;
margin-right: 4px;
}
.potion-haut-parler {
font-style: italic;
color: #5a3a8a;
}
}
}
// Post-item card: danger divider variant (for létal effects)
.mournblade-item-card {
.item-card-divider--danger {
color: #8b0000;
&::before, &::after {
background: linear-gradient(90deg, transparent, #8b0000, transparent);
}
}
}
+480
View File
@@ -0,0 +1,480 @@
/* ==================== Item Sheet Styles ==================== */
/* Item header with image and name */
.fvtt-mournblade.item {
/* Background pour toute la fiche d'item */
background: url("../assets/ui/pc_sheet_bg.webp") repeat;
display: flex;
flex-direction: column;
height: 100%;
padding: 0;
margin: 0;
/* AppV2 - Remove window content padding */
.window-content {
padding: 0;
margin: 0;
}
/* AppV2 - Main section structure */
section {
background: url("../assets/ui/pc_sheet_bg.webp") repeat-y;
color: black;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
padding: 0;
margin: 0;
}
/* AppV2 Item Sheets - Disabled inputs readability */
input:disabled,
select:disabled {
color: #000000;
opacity: 0.8;
background-color: rgba(255, 255, 255, 0.5);
}
/* Inputs and selects styling */
input[type="text"],
input[type="number"],
select {
color: #000000;
background-color: rgba(255, 255, 255, 0.7);
border: 1px solid #999999;
margin: 0;
padding: 2px 4px;
font-family: "CentaurMT", serif;
font-size: 0.85rem;
}
textarea {
margin: 0;
padding: 2px 4px;
}
input[type="checkbox"] {
width: auto;
height: auto;
margin: 0 4px;
align-self: center;
}
.header {
flex: 0 0 auto;
border-bottom: 1px solid #999;
margin: 0;
}
.sheet-header {
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
padding: 0.5rem;
background: url("../assets/ui/pc_sheet_bg.webp") repeat;
flex: 0 0 auto;
}
.item-sheet-img {
flex: 0 0 64px;
width: 64px;
height: 64px;
object-fit: cover;
border: 1px solid #999;
border-radius: 4px;
cursor: pointer;
&:hover {
box-shadow: 0 0 8px rgba(255, 102, 0, 0.8);
}
}
.header-fields {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.header-actions {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 0.5rem;
button {
background: rgba(0, 0, 0, 0.1);
border: 1px solid #999;
padding: 0.25rem 0.5rem;
cursor: pointer;
font-family: "CentaurMT", serif;
&:hover {
background: rgba(255, 102, 0, 0.2);
box-shadow: 0 0 5px rgba(255, 102, 0, 0.5);
}
}
}
.chat-card-button {
background: linear-gradient(to bottom, #21374afc 5%, #152833ab 100%);
border: 2px ridge #846109;
color: #d4b5a8;
padding: 0.3rem 0.5rem;
transition: all 0.2s ease;
cursor: pointer;
i {
font-size: 0.9rem;
}
&:hover {
background: linear-gradient(to bottom, #800000 5%, #3e0101 100%);
color: #ffffff;
box-shadow: 0 0 8px rgba(128, 0, 0, 0.6);
}
&:active {
position: relative;
top: 1px;
}
}
.sheet-header h1.charname {
height: 50px;
padding: 0;
margin: 5px 0;
border-bottom: 0;
font-weight: bold;
font-size: 2rem;
font-family: "CentaurMT";
}
.sheet-header h1.charname input {
width: 100%;
height: 100%;
margin: 0;
font-weight: bold;
font-family: "CentaurMT";
font-size: 2rem;
text-align: left;
border: 0 none;
&:focus {
outline: 1px solid #ff6600;
}
}
/* Tabs - Modern style matching actor sheets */
nav.tabs {
display: flex;
border-bottom: 1px solid #7a7971;
margin: 0;
padding: 4px;
background-color: #1c1c1c;
flex: 0 0 auto;
}
nav.tabs a.item {
padding: 6px 12px;
color: #d4af37;
text-decoration: none;
border: 1px solid transparent;
border-radius: 4px 4px 0 0;
margin-right: 4px;
transition: all 0.2s;
i {
display: none; // Hide icons to match actor sheets
}
&:hover {
background-color: rgba(212, 175, 55, 0.1);
}
&.active {
background-color: #2a2a2a;
border-bottom-color: #d4af37;
color: #f5f5f5;
}
}
/* Tab content */
.tab {
display: none;
padding: 4px 8px;
overflow-y: auto;
flex: 1 1 auto;
&.active {
display: block;
}
}
/* Item list in details tab */
.item-list {
list-style: none;
margin: 0;
padding: 0;
li.item {
display: flex;
align-items: center;
margin-bottom: 2px;
padding: 2px 4px;
min-height: 24px;
border-bottom: none;
&.flexrow {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 4px;
input[type="checkbox"] {
margin-top: 3px;
flex-shrink: 0;
}
}
h3 {
color: #2a1a0a;
font-size: 1rem;
font-weight: bold;
margin: 0;
text-shadow: none;
}
}
}
/* Labels */
.generic-label {
display: inline-block;
white-space: nowrap;
font-weight: 700;
color: #464331c4;
font-size: 0.9rem;
font-family: "CentaurMT", serif;
margin: 0;
padding: 0 4px 0 0;
}
/* Field labels */
.item-field-label-short {
flex: 0 0 60px;
max-width: 60px;
}
.item-field-label-medium {
flex: 0 0 100px;
max-width: 100px;
}
.item-field-label-long {
flex: 1;
min-width: 150px;
}
.item-field-label-long1 {
flex: 1;
min-width: 200px;
}
.item-field-label-long2 {
flex: 1;
min-width: 250px;
max-width: 250px;
}
.item-field-label-long3 {
flex: 1;
min-width: 250px;
white-space: normal;
line-height: 1.3;
}
.numeric-input {
text-align: center;
width: 60px;
}
/* Editor fields */
.editor {
height: 300px;
border: 1px solid #999;
background: rgba(255, 255, 255, 0.9);
.editor-content {
height: 100%;
padding: 0.5rem;
color: #000;
}
}
}
/* ============================================= */
/* Potion Item Sheet */
/* ============================================= */
.potion-content {
.potion-header-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 0.5rem;
background: rgba(139, 0, 0, 0.08);
border-radius: 4px;
border: 1px solid rgba(139, 0, 0, 0.2);
margin-bottom: 0.5rem;
}
.potion-statut-badge {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.2rem 0.6rem;
border-radius: 12px;
font-size: 0.85rem;
font-weight: bold;
&.statut-efficace { background: rgba(0,128,0,0.15); color: #155215; border: 1px solid #4a904a; }
&.statut-heroique { background: rgba(180,120,0,0.15); color: #7a5000; border: 1px solid #c89000; }
&.statut-inefficace { background: rgba(100,100,100,0.15); color: #555; border: 1px solid #888; }
&.statut-poison { background: rgba(100,0,100,0.15); color: #4a004a; border: 1px solid #880088; }
&.statut-inconnue { background: rgba(50,50,50,0.1); color: #666; border: 1px solid #aaa; }
}
}
/* Potion in chat */
.potion-rune-info {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
padding: 0.4rem;
background: rgba(0,0,0,0.04);
border-radius: 4px;
margin-bottom: 0.35rem;
.potion-rune-name { font-weight: bold; }
.potion-rune-seuil { font-size: 0.85rem; color: #666; }
.potion-haut-parler { font-size: 0.85rem; color: #555; font-style: italic; }
}
.potion-result-gm {
border: 1px dashed rgba(139,0,0,0.4);
background: rgba(139,0,0,0.05);
border-radius: 4px;
padding: 0.4rem;
.potion-result-title {
font-size: 0.95rem;
margin: 0 0 0.3rem;
}
.potion-heroique { color: #8B6900; }
.potion-efficace { color: #155215; }
.potion-inefficace { color: #555; }
.potion-poison { color: #660066; }
}
.potion-result-player {
font-style: italic;
color: #666;
font-size: 0.9rem;
}
/* Potion dialog */
.potion-dialog {
.potion-dialog-section {
margin-bottom: 0.75rem;
}
.potion-dialog-title {
font-size: 0.9rem;
font-weight: bold;
border-bottom: 1px solid rgba(0,0,0,0.15);
padding-bottom: 0.2rem;
margin-bottom: 0.4rem;
}
.potion-runes-table {
width: 100%;
font-size: 0.85rem;
border-collapse: collapse;
margin-bottom: 0.5rem;
th, td {
padding: 0.2rem 0.4rem;
border-bottom: 1px solid rgba(0,0,0,0.08);
}
.potion-rune-row:hover { background: rgba(0,0,0,0.04); }
.potion-rune-radio { cursor: pointer; }
.rune-mini-img {
width: 20px;
height: 20px;
vertical-align: middle;
margin-right: 4px;
border-radius: 2px;
border: 1px solid rgba(139, 69, 19, 0.5);
object-fit: cover;
}
}
.potion-summary {
background: rgba(0,0,0,0.05);
border-radius: 4px;
padding: 0.4rem 0.6rem;
font-size: 0.85rem;
.summary-row { margin-bottom: 0.2rem; }
.summary-label { font-weight: bold; min-width: 150px; }
}
.potion-info-note {
color: #888;
font-style: italic;
gap: 0.4rem;
align-items: center;
}
}
/* Enchantement de la Loi */
.enchantement-section {
margin-top: 0.6rem;
padding: 0.5rem 0.6rem;
background: rgba(255, 215, 0, 0.05);
border: 1px solid rgba(255, 215, 0, 0.3);
border-radius: 4px;
.section-title-small {
font-size: 0.9rem;
margin: 0 0 0.4rem 0;
color: #a07800;
font-weight: bold;
display: flex;
align-items: center;
gap: 0.3rem;
border-bottom: 1px solid rgba(255, 215, 0, 0.2);
padding-bottom: 0.2rem;
}
.enchant-badge {
display: inline-block;
background: gold;
color: #333;
font-size: 0.7rem;
padding: 0.1rem 0.4rem;
border-radius: 8px;
margin-left: 0.5rem;
}
.enchant-none {
color: #888;
font-size: 0.85rem;
margin: 0;
padding: 0.2rem 0;
}
}
+6
View File
@@ -0,0 +1,6 @@
// Main LESS file for Mournblade system
// Importing base styles and component-specific styles
@import "simple-converted";
@import "item-styles";
@import "actor-styles";
File diff suppressed because it is too large Load Diff
@@ -1,18 +1,18 @@
/**
* Extend the basic ActorSheet with some very simple modifications
* @extends {ActorSheet}
* @extends {ActorSheetV2}
*/
import { MournbladeUtility } from "./mournblade-utility.js";
import { MournbladeRollDialog } from "./mournblade-roll-dialog.js";
/* -------------------------------------------- */
export class MournbladeActorSheet extends ActorSheet {
export class MournbladeActorSheet extends foundry.applications.sheets.ActorSheetV2 {
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["fvtt-mournblade", "sheet", "actor"],
template: "systems/fvtt-mournblade/templates/actor-sheet.html",
width: 640,
@@ -25,8 +25,8 @@ export class MournbladeActorSheet extends ActorSheet {
/* -------------------------------------------- */
async getData() {
const objectData = duplicate(this.object)
let actorData = objectData
const objectData = foundry.utils.duplicate(this.object)
let actorData = objectData
let formData = {
title: this.title,
@@ -40,22 +40,30 @@ export class MournbladeActorSheet extends ActorSheet {
effects: this.object.effects.map(e => foundry.utils.deepClone(e.data)),
limited: this.object.limited,
skills: this.actor.getSkills(),
armes: duplicate(this.actor.getWeapons()),
protections: duplicate(this.actor.getArmors()),
dons: duplicate(this.actor.getDons()),
armes: foundry.utils.duplicate(this.actor.getWeapons()),
protections: foundry.utils.duplicate(this.actor.getArmors()),
dons: foundry.utils.duplicate(this.actor.getDons()),
pactes: foundry.utils.duplicate(this.actor.getPactes()),
alignement: this.actor.getAlignement(),
aspect: this.actor.getAspect(),
marge: this.actor.getMarge(),
tendances:duplicate(this.actor.getTendances()),
runes:duplicate(this.actor.getRunes()),
traitsChaotiques:duplicate(this.actor.getTraitsChaotiques()),
origine: duplicate(this.actor.getOrigine() || {}),
heritage: duplicate(this.actor.getHeritage() || {}),
metier: duplicate(this.actor.getMetier() || {}),
combat: this.actor.getCombatValues(),
equipements: duplicate(this.actor.getEquipments()),
monnaies: duplicate(this.actor.getMonnaies()),
description: await TextEditor.enrichHTML(this.object.system.biodata.description, {async: true}),
tendances:foundry.utils.duplicate(this.actor.getTendances()),
runes:foundry.utils.duplicate(this.actor.getRunes()),
traitsChaotiques:foundry.utils.duplicate(this.actor.getTraitsChaotiques()),
traitsEspeces: foundry.utils.duplicate(this.actor.getTraitsEspeces()),
origine: foundry.utils.duplicate(this.actor.getOrigine() || {}),
heritage: foundry.utils.duplicate(this.actor.getHeritage() || {}),
metier: foundry.utils.duplicate(this.actor.getMetier() || {}),
combat: this.actor.getCombatValues(),
equipements: foundry.utils.duplicate(this.actor.getEquipments()),
modifiers: foundry.utils.duplicate(this.actor.getModifiers()),
monnaies: foundry.utils.duplicate(this.actor.getMonnaies()),
runeEffects: foundry.utils.duplicate(this.actor.getRuneEffects()),
config: game.system.mournblade.config,
protectionTotal: this.actor.getProtectionTotal(),
santeMalus: this.actor.getStatusMalus(),
ameMalus: this.actor.getAmeMalus(),
description: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.object.system.biodata.description, {async: true}),
options: this.options,
owner: this.document.isOwner,
editScore: this.options.editScore,
@@ -67,7 +75,7 @@ export class MournbladeActorSheet extends ActorSheet {
return formData;
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
@@ -75,14 +83,14 @@ export class MournbladeActorSheet extends ActorSheet {
// Everything below here is only needed if the sheet is editable
if (!this.options.editable) return;
// Update Inventory Item
html.find('.item-edit').click(ev => {
const li = $(ev.currentTarget).parents(".item")
let itemId = li.data("item-id")
const item = this.actor.items.get( itemId )
item.sheet.render(true)
})
})
// Delete Inventory Item
html.find('.item-delete').click(ev => {
const li = $(ev.currentTarget).parents(".item");
@@ -97,7 +105,7 @@ export class MournbladeActorSheet extends ActorSheet {
let value = ev.currentTarget.value
this.actor.editItemField(itemId, itemType, itemField, dataType, value)
})
html.find('.quantity-minus').click(event => {
const li = $(event.currentTarget).parents(".item");
this.actor.incDecQuantity( li.data("item-id"), -1 );
@@ -128,6 +136,21 @@ export class MournbladeActorSheet extends ActorSheet {
let armeId = li.data("item-id")
this.actor.rollArmeOffensif(armeId)
})
html.find('.roll-assomer').click((event) => {
this.actor.rollAssomer()
})
html.find('.roll-fuir').click((event) => {
this.actor.rollFuir()
})
html.find('.roll-immobiliser').click((event) => {
this.actor.rollImmobiliser()
})
html.find('.roll-arme-special').click((event) => {
const li = $(event.currentTarget).parents(".item")
let armeId = li.data("item-id")
this.actor.rollArmeSpecial(armeId)
})
html.find('.roll-arme-degats').click((event) => {
const li = $(event.currentTarget).parents(".item")
let armeId = li.data("item-id")
@@ -142,20 +165,29 @@ export class MournbladeActorSheet extends ActorSheet {
const itemType = $(event.currentTarget).data("type")
this.actor.createEmbeddedDocuments('Item', [{ name: `Nouveau ${itemType}`, type: itemType }], { renderSheet: true })
})
html.find('.sante-modify').click((event) => {
const santeType = $(event.currentTarget).data("type")
const value = $(event.currentTarget).data("value")
this.actor.incDecSante(santeType, value, false)
})
html.find('.ame-modify').click((event) => {
const value = $(event.currentTarget).data("value")
this.actor.incDecAme(value)
})
html.find('.lock-unlock-sheet').click((event) => {
this.options.editScore = !this.options.editScore;
this.render(true);
});
});
html.find('.item-equip').click(ev => {
const li = $(ev.currentTarget).parents(".item");
this.actor.equipItem( li.data("item-id") );
this.render(true);
this.render(true);
});
}
/* -------------------------------------------- */
/** @override */
setPosition(options = {}) {
@@ -0,0 +1,25 @@
/**
* Extend the basic ActorSheet with some very simple modifications
* @extends {ActorSheet}
*/
import { MournbladeActorSheet } from "./mournblade-actor-sheet.js";
import { MournbladeUtility } from "./mournblade-utility.js";
/* -------------------------------------------- */
export class MournbladeCreatureSheet extends MournbladeActorSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["fvtt-mournblade", "sheet", "actor"],
template: "systems/fvtt-mournblade/templates/creature-sheet.html",
width: 640,
height: 720,
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "stats" }],
dragDrop: [{ dragSelector: ".item-list .item", dropSelector: null }],
editScore: false
})
}
}
@@ -0,0 +1,157 @@
import { MournbladeUtility } from "../mournblade-utility.js"
export default class MournbladeEnchantementDialog {
static _normalize(str) {
return (str ?? "")
.toLowerCase()
.replace(/œ/g, "oe")
.replace(/æ/g, "ae")
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-z0-9]+/g, " ")
.trim()
}
static _findCompetence(actor, ...keywords) {
const normKeys = keywords.map(k => MournbladeEnchantementDialog._normalize(k))
return actor.items.find(item => {
if (item.type !== "competence") return false
const norm = MournbladeEnchantementDialog._normalize(item.name)
return normKeys.every(k => norm.includes(k))
}) ?? null
}
static async create(actor, item) {
const normalize = MournbladeEnchantementDialog._normalize.bind(MournbladeEnchantementDialog)
const findComp = (...kw) => MournbladeEnchantementDialog._findCompetence(actor, ...kw)
const ameDisponible = Math.max(0, (actor.system.ame.currentmax - actor.system.ame.value))
const aspect = actor.system.balance.aspect ?? 0
// Skill lookups
const savoirRunesComp = findComp("rune")
const hautParlerComp = findComp("haut", "parler")
const artisanatComp = findComp("savoir", "artisanat")
const claAttr = actor.system.attributs?.clairvoyance
// Prerequisite: Rune de la Loi in inventory
const hasRuneLoi = actor.items.some(i => {
if (i.type !== "rune") return false
return normalize(i.name).includes("loi")
})
const savoirRunesNiveau = savoirRunesComp ? (savoirRunesComp.system.niveau ?? 0) : null
const hautParlerNiveau = hautParlerComp ? (hautParlerComp.system.niveau ?? 0) : null
const artisanatNiveau = artisanatComp ? (artisanatComp.system.niveau ?? 0) : null
const claValeur = claAttr ? (claAttr.value ?? 0) : 0
// Limit: CLA + Savoir:Runes is capped by min(Haut-Parler, Artisanat) if those skills exist
const limiteur = (hautParlerNiveau !== null && artisanatNiveau !== null)
? Math.min(hautParlerNiveau, artisanatNiveau)
: (hautParlerNiveau ?? artisanatNiveau ?? null)
const context = {
actorImg: actor.img,
actorName: actor.name,
itemImg: item.img,
itemName: item.name,
itemType: item.type,
itemId: item.id,
ameDisponible,
aspect,
hasRuneLoi,
savoirRunesNiveau,
hautParlerNiveau,
artisanatNiveau,
claValeur,
limiteur,
modOptions: Array.from({ length: 21 }, (_, i) => i - 10),
enchantementActif: item.system.enchantementLoi?.actif ?? false,
enchantementBonus: item.system.enchantementLoi?.bonus ?? 0,
enchantementAntiChaos: item.system.enchantementLoi?.antiChaos ?? false,
}
const prerequisOk = hasRuneLoi
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-mournblade/templates/dialog-enchantement.hbs",
context
)
Hooks.once("renderDialogV2", (_app, html) => {
const form = html.querySelector ? html : html[0]
MournbladeEnchantementDialog._attachListeners(form, ameDisponible, claValeur, savoirRunesNiveau, limiteur, prerequisOk)
})
return foundry.applications.api.DialogV2.wait({
window: { title: `Enchanter : ${item.name}`, icon: "fa-solid fa-star" },
classes: ["mournblade-roll-dialog"],
position: { width: 520 },
modal: false,
content,
buttons: [
{
action: "enchanter",
label: "Enchanter",
icon: "fa-solid fa-star",
default: true,
callback: async (event, button, dialog) => {
const elems = button.form.elements
const ptsAme = parseInt(elems["ptsAme"]?.value ?? 5) || 5
const antiChaos = elems["antiChaos"]?.value === "true"
const modificateur = parseInt(elems["modificateur"]?.value ?? 0) || 0
await MournbladeUtility.rollEnchantement({
actor,
item,
ptsAme,
antiChaos,
modificateur,
savoirRunesComp,
hautParlerComp,
artisanatComp,
claValeur,
limiteur,
})
}
},
],
rejectClose: false,
})
}
static _attachListeners(html, ameDisponible, claValeur, savoirRunesNiveau, limiteur, prerequisOk = true) {
const enchanterBtn = html.querySelector('button[data-action="enchanter"]')
if (enchanterBtn) enchanterBtn.disabled = !prerequisOk
const diffEl = html.querySelector('#enchant-difficulte')
const bonusEl = html.querySelector('#enchant-bonus-preview')
const warnAmeEl = html.querySelector('#enchant-ame-warn')
const warnLimitEl = html.querySelector('#enchant-limit-warn')
const totalEl = html.querySelector('#enchant-total-dice')
const recalculate = () => {
const ptsAme = parseInt(html.querySelector('[name="ptsAme"]')?.value ?? 5) || 0
const difficulte = ptsAme
const bonus = Math.floor(ptsAme / 5)
const savoir = savoirRunesNiveau ?? 0
const basePool = claValeur + savoir
const effectivePool = limiteur !== null ? Math.min(basePool, limiteur) : basePool
if (diffEl) diffEl.textContent = difficulte
if (bonusEl) bonusEl.textContent = `+${bonus}`
if (totalEl) totalEl.textContent = effectivePool
if (warnAmeEl) warnAmeEl.style.display = ptsAme > ameDisponible ? "" : "none"
if (warnLimitEl && limiteur !== null)
warnLimitEl.style.display = basePool > limiteur ? "" : "none"
}
const ptsAmeEl = html.querySelector('[name="ptsAme"]')
if (ptsAmeEl) {
ptsAmeEl.addEventListener('input', recalculate)
ptsAmeEl.addEventListener('change', recalculate)
}
recalculate()
}
}
@@ -0,0 +1,174 @@
import { MournbladeUtility } from "../mournblade-utility.js"
export default class MournbladeInvocationDemonDialog {
/**
* Normalize a string for fuzzy matching: lowercase, remove diacritics,
* replace œ/æ ligatures, collapse whitespace and punctuation.
*/
static _normalize(str) {
return (str ?? "")
.toLowerCase()
.replace(/œ/g, "oe")
.replace(/æ/g, "ae")
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "") // strip combining diacritics
.replace(/[^a-z0-9]+/g, " ") // replace any non-alnum with space
.trim()
}
/**
* Returns the first competence item whose name contains all of the given keywords.
* Keywords are normalized before comparison.
*/
static _findCompetence(actor, ...keywords) {
const normKeys = keywords.map(k => MournbladeInvocationDemonDialog._normalize(k))
return actor.items.find(item => {
if (item.type !== "competence") return false
const norm = MournbladeInvocationDemonDialog._normalize(item.name)
return normKeys.every(k => norm.includes(k))
}) ?? null
}
/**
* Returns true if the actor has a capacite or don whose name contains all given keywords.
*/
static _hasCapacite(actor, ...keywords) {
const normKeys = keywords.map(k => MournbladeInvocationDemonDialog._normalize(k))
return actor.items.some(item => {
if (item.type !== "capacite" && item.type !== "don") return false
const norm = MournbladeInvocationDemonDialog._normalize(item.name)
return normKeys.every(k => norm.includes(k))
})
}
static async create(actor, rollData) {
const ameDisponible = Math.max(0, (actor.system.ame.currentmax - actor.system.ame.value))
// Robust skill detection: partial keyword matching, diacritic-insensitive
const coercitionComp = MournbladeInvocationDemonDialog._findCompetence(actor, "coercition")
const hautParlerComp = MournbladeInvocationDemonDialog._findCompetence(actor, "haut", "parler")
const loiChaosComp = MournbladeInvocationDemonDialog._findCompetence(actor, "loi", "chaos")
// Check prerequisites — robust capacite/rune detection
const isChaotique = actor.system.balance.chaos > actor.system.balance.loi
const hasOeilSorcier = MournbladeInvocationDemonDialog._hasCapacite(actor, "oeil", "sorcier")
const hasRuneChaos = actor.items.some(i => {
if (i.type !== "rune") return false
return MournbladeInvocationDemonDialog._normalize(i.name).includes("chaos")
})
const prerequisOk = isChaotique && hasOeilSorcier && hasRuneChaos
// Auto-detect chaos link bonuses
const aspectGe8 = (actor.system.balance.aspect ?? 0) >= 8
// hasPacte: true if actor has a Pacte item whose associated deity matches a demon
// (simplified: just expose it as a selectable option — can't auto-detect)
const hasPacte = false
const context = {
...rollData,
img: actor.img,
name: actor.name,
ameDisponible,
modOptions: Array.from({ length: 21 }, (_, i) => i - 10),
coercitionNiveau: coercitionComp ? coercitionComp.system.niveau : null,
hautParlerNiveau: hautParlerComp ? hautParlerComp.system.niveau : null,
loiChaosNiveau: loiChaosComp ? loiChaosComp.system.niveau : null,
isChaotique,
hasOeilSorcier,
hasRuneChaos,
prerequisOk,
aspectGe8,
hasPacte,
}
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-mournblade/templates/dialog-invocation-demon.hbs",
context
)
Hooks.once("renderDialogV2", (_app, html) => {
const form = html.querySelector ? html : html[0]
MournbladeInvocationDemonDialog._attachListeners(form, ameDisponible, prerequisOk)
})
return foundry.applications.api.DialogV2.wait({
window: { title: "Invoquer un Démon", icon: "fa-solid fa-skull" },
classes: ["mournblade-roll-dialog"],
position: { width: 560 },
modal: false,
content,
buttons: [
{
action: "invoquer",
label: "Invoquer",
icon: "fa-solid fa-skull",
default: true,
callback: async (event, button, dialog) => {
MournbladeInvocationDemonDialog._updateRollData(rollData, button.form.elements, actor, {
coercitionComp, hautParlerComp, loiChaosComp
})
await MournbladeUtility.rollInvocationDemon(rollData)
}
},
],
rejectClose: false,
})
}
static _calculateSeuil(html) {
const get = (name) => parseInt(html.querySelector(`[name="${name}"]`)?.value ?? 0) || 0
const seuil = get("seuil_nature")
+ get("seuil_traits")
+ get("seuil_augmentation")
+ get("seuil_service")
+ get("seuil_duree")
+ get("seuil_marche")
+ get("seuil_chaos")
+ get("seuil_sacrifice")
return Math.max(1, seuil)
}
static _attachListeners(html, ameDisponible, prerequisOk = true) {
const invoquerBtn = html.querySelector('button[data-action="invoquer"]')
if (invoquerBtn) invoquerBtn.disabled = !prerequisOk
const coutEl = html.querySelector('#invoc-demon-cout')
const totalEl = html.querySelector('#invoc-demon-seuil-total')
const hiddenEl = html.querySelector('#invoc-demon-seuil-hidden')
const warnEl = html.querySelector('#invoc-demon-ame-warn')
const criteriaNames = [
"seuil_nature", "seuil_traits", "seuil_augmentation",
"seuil_service", "seuil_duree", "seuil_marche",
"seuil_chaos", "seuil_sacrifice"
]
const recalculate = () => {
const seuil = MournbladeInvocationDemonDialog._calculateSeuil(html)
if (totalEl) totalEl.textContent = seuil
if (hiddenEl) hiddenEl.value = seuil
if (coutEl) coutEl.textContent = seuil
if (warnEl) warnEl.style.display = seuil > ameDisponible ? "" : "none"
}
for (const name of criteriaNames) {
const el = html.querySelector(`[name="${name}"]`)
if (el) el.addEventListener('change', recalculate)
if (el && el.type === 'number') el.addEventListener('input', recalculate)
}
recalculate()
}
static _updateRollData(rollData, formElements, actor, { coercitionComp }) {
const seuil = parseInt(formElements['seuil']?.value ?? 5)
const modificateur = parseInt(formElements['modificateur']?.value ?? 0)
rollData.invocationSeuil = seuil
rollData.invocationSoulCost = seuil
rollData.difficulte = seuil
rollData.modificateur = modificateur
rollData.competence = coercitionComp ?? null
}
}
@@ -0,0 +1,154 @@
import { MournbladeUtility } from "../mournblade-utility.js"
export default class MournbladeInvocationDialog {
/**
* Normalize a string for fuzzy matching: lowercase, remove diacritics,
* replace œ/æ ligatures, collapse whitespace and punctuation.
*/
static _normalize(str) {
return (str ?? "")
.toLowerCase()
.replace(/œ/g, "oe")
.replace(/æ/g, "ae")
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-z0-9]+/g, " ")
.trim()
}
/**
* Returns the first competence item whose name contains all of the given keywords.
*/
static _findCompetence(actor, ...keywords) {
const normKeys = keywords.map(k => MournbladeInvocationDialog._normalize(k))
return actor.items.find(item => {
if (item.type !== "competence") return false
const norm = MournbladeInvocationDialog._normalize(item.name)
return normKeys.every(k => norm.includes(k))
}) ?? null
}
static async create(actor, rollData) {
const ameDisponible = Math.max(0, (actor.system.ame.currentmax - actor.system.ame.value))
const maxExtra = Math.max(0, ameDisponible - 15)
// Detect elemental pacte bonus — the bonus is always available,
// but we display it dynamically based on chosen element.
// We pass the pactes to let JS listeners detect the match.
const pactes = actor.getPactes().map(p => ({
name: p.name,
allegeance: (p.system.allegeance || "").toLowerCase()
}))
// Robust skill detection: partial keyword matching, diacritic-insensitive
const hautParlerComp = MournbladeInvocationDialog._findCompetence(actor, "haut", "parler")
const seigneursElemComp = MournbladeInvocationDialog._findCompetence(actor, "seigneurs", "elementaires")
const context = {
...rollData,
img: actor.img,
name: actor.name,
ameDisponible,
ameExtraOptions: Array.from({ length: maxExtra + 1 }, (_, i) => i),
modOptions: Array.from({ length: 21 }, (_, i) => i - 10),
hautParlerNiveau: hautParlerComp ? hautParlerComp.system.niveau : null,
seigneursElemNiveau: seigneursElemComp ? seigneursElemComp.system.niveau : null,
bonusPacte: false,
pactes,
}
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-mournblade/templates/dialog-invocation-elementaire.hbs",
context
)
Hooks.once("renderDialogV2", (_app, html) => {
const form = html.querySelector ? html : html[0]
MournbladeInvocationDialog._attachListeners(form, pactes)
})
return foundry.applications.api.DialogV2.wait({
window: { title: "Invoquer un Élémentaire", icon: "fa-solid fa-wind" },
classes: ["mournblade-roll-dialog"],
position: { width: 480 },
modal: false,
content,
buttons: [
{
action: "invoquer",
label: "Invoquer",
icon: "fa-solid fa-wind",
default: true,
callback: async (event, button, dialog) => {
MournbladeInvocationDialog._updateRollData(rollData, button.form.elements, actor, pactes)
await MournbladeUtility.rollInvocationElementaire(rollData)
}
},
],
rejectClose: false,
})
}
static _elementKeywords = {
air: ["air", "vent", "sylphe", "seigneur de l'air"],
terre: ["terre", "gnome", "seigneur de la terre"],
feu: ["feu", "flamme", "salamandre", "seigneur du feu"],
eau: ["eau", "ondine", "seigneur de l'eau"],
}
static _hasPacteBonus(element, pactes) {
const keywords = MournbladeInvocationDialog._elementKeywords[element] || []
return pactes.some(p => keywords.some(kw => p.allegeance.includes(kw)))
}
static _attachListeners(html, pactes) {
const tierSeuils = { mineur: 15, median: 20, majeur: 25 }
const tierTemps = { mineur: "1 tour", median: "1 minute", majeur: "1 heure" }
const recalculate = () => {
const element = html.querySelector('[name="element"]')?.value ?? "air"
const tier = html.querySelector('[name="tier"]')?.value ?? "mineur"
const ameExtra = parseInt(html.querySelector('[name="ameExtra"]')?.value ?? 0)
const seuilBase = tierSeuils[tier] ?? 15
const seuil = seuilBase + ameExtra
const hasPacte = MournbladeInvocationDialog._hasPacteBonus(element, pactes)
const seuilEl = html.querySelector('#invoc-seuil')
const coutEl = html.querySelector('#invoc-cout')
const tempsEl = html.querySelector('#invoc-temps')
const pacteBanner = html.querySelector('.invoc-bonus-pacte')
if (seuilEl) seuilEl.textContent = seuil + (hasPacte ? " (-5 avec bonus Pacte)" : "")
if (coutEl) coutEl.textContent = seuil
if (tempsEl) tempsEl.textContent = tierTemps[tier] ?? "1 tour"
if (pacteBanner) pacteBanner.style.display = hasPacte ? "" : "none"
}
html.querySelectorAll('[name="element"], [name="tier"], [name="ameExtra"]').forEach(el => {
el.addEventListener('change', recalculate)
})
recalculate()
}
static _updateRollData(rollData, formElements, actor, pactes) {
const element = formElements['element']?.value ?? "air"
const tier = formElements['tier']?.value ?? "mineur"
const ameExtra = parseInt(formElements['ameExtra']?.value ?? 0)
const modificateur = parseInt(formElements['modificateur']?.value ?? 0)
const tierSeuils = { mineur: 15, median: 20, majeur: 25 }
const seuilBase = tierSeuils[tier] ?? 15
const seuil = seuilBase + ameExtra
rollData.invocationElement = element
rollData.invocationTier = tier
rollData.invocationSeuil = seuil
rollData.invocationAmeExtra = ameExtra
rollData.invocationSoulCost = seuil
rollData.difficulte = seuil
rollData.modificateur = modificateur
rollData.bonusPacte = MournbladeInvocationDialog._hasPacteBonus(element, pactes) ? 5 : 0
}
}
@@ -0,0 +1,138 @@
import { MournbladeUtility } from "../mournblade-utility.js"
export default class MournbladeInvocationEspritDialog {
static _normalize(str) {
return (str ?? "")
.toLowerCase()
.replace(/œ/g, "oe").replace(/æ/g, "ae")
.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-z0-9]+/g, " ").trim()
}
static _findCompetence(actor, ...keywords) {
const normKeys = keywords.map(k => MournbladeInvocationEspritDialog._normalize(k))
return actor.items.find(item => {
if (item.type !== "competence") return false
const norm = MournbladeInvocationEspritDialog._normalize(item.name)
return normKeys.every(k => norm.includes(k))
}) ?? null
}
/** Seuil and soul cost per puissance */
static SEUILS = { mineur: 15, median: 20, majeur: 25 }
static async create(actor, rollData) {
const ameDisponible = Math.max(0, actor.system.ame.currentmax - actor.system.ame.value)
const persuasionComp = MournbladeInvocationEspritDialog._findCompetence(actor, "persuasion")
const hautParlerComp = MournbladeInvocationEspritDialog._findCompetence(actor, "haut", "parler")
const loiChaosComp = MournbladeInvocationEspritDialog._findCompetence(actor, "loi", "chaos")
const isLoyal = actor.system.balance.loi > actor.system.balance.chaos
const hasRuneLoi = actor.items.some(i =>
i.type === "rune" && MournbladeInvocationEspritDialog._normalize(i.name).includes("loi")
)
const prerequisOk = isLoyal && hasRuneLoi
const automatonTypes = {
combat: "Combat",
voyage: "Voyage",
perception: "Perception",
restauration:"Restauration",
reparateur: "Réparateur",
}
const context = {
...rollData,
img: actor.img,
name: actor.name,
ameDisponible,
treValeur: actor.system.attributs?.tre?.value ?? 0,
persuasionNiveau: persuasionComp?.system?.niveau ?? null,
hautParlerNiveau: hautParlerComp?.system?.niveau ?? null,
loiChaosNiveau: loiChaosComp?.system?.niveau ?? null,
isLoyal,
hasRuneLoi,
prerequisOk,
automatonTypes,
seuils: MournbladeInvocationEspritDialog.SEUILS,
modOptions: Array.from({ length: 21 }, (_, i) => i - 10),
}
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-mournblade/templates/dialog-invocation-esprit.hbs",
context
)
Hooks.once("renderDialogV2", (_app, html) => {
const form = html.querySelector ? html : html[0]
MournbladeInvocationEspritDialog._attachListeners(form, ameDisponible, prerequisOk)
})
return foundry.applications.api.DialogV2.wait({
window: { title: "Invoquer un Esprit de la Loi", icon: "fa-solid fa-star" },
classes: ["mournblade-roll-dialog"],
position: { width: 520 },
modal: false,
content,
buttons: [
{
action: "invoquer",
label: "Invoquer",
icon: "fa-solid fa-star",
default: true,
callback: async (event, button, dialog) => {
MournbladeInvocationEspritDialog._updateRollData(rollData, button.form.elements, actor, {
persuasionComp, hautParlerComp, loiChaosComp
})
await MournbladeUtility.rollInvocationEsprit(rollData)
}
},
],
rejectClose: false,
})
}
static _attachListeners(html, ameDisponible, prerequisOk = true) {
const invoquerBtn = html.querySelector('button[data-action="invoquer"]')
if (invoquerBtn) invoquerBtn.disabled = !prerequisOk
const puissanceEl = html.querySelector('[name="puissance"]')
const seuilEl = html.querySelector('#esprit-seuil-total')
const coutEl = html.querySelector('#esprit-cout-ame')
const hiddenEl = html.querySelector('#esprit-seuil-hidden')
const warnEl = html.querySelector('#esprit-ame-warn')
const dureeEl = html.querySelector('#esprit-duree')
const DUREE = { mineur: "1 heure", median: "1 jour", majeur: "1 semaine" }
const recalculate = () => {
const puissance = puissanceEl?.value ?? "mineur"
const seuil = MournbladeInvocationEspritDialog.SEUILS[puissance] ?? 15
if (seuilEl) seuilEl.textContent = seuil
if (coutEl) coutEl.textContent = seuil
if (hiddenEl) hiddenEl.value = seuil
if (dureeEl) dureeEl.textContent = DUREE[puissance] ?? "1 heure"
if (warnEl) warnEl.style.display = seuil > ameDisponible ? "" : "none"
}
if (puissanceEl) puissanceEl.addEventListener("change", recalculate)
recalculate()
}
static _updateRollData(rollData, formElements, actor, { persuasionComp }) {
const seuil = parseInt(formElements["seuil"]?.value ?? 15)
const modificateur = parseInt(formElements["modificateur"]?.value ?? 0)
const automatonType = formElements["automatonType"]?.value ?? "combat"
const puissance = formElements["puissance"]?.value ?? "mineur"
rollData.invocationSeuil = seuil
rollData.invocationSoulCost = seuil
rollData.difficulte = seuil
rollData.modificateur = modificateur
rollData.competence = persuasionComp ?? null
rollData.automatonType = automatonType
rollData.automatonPuissance = puissance
}
}
@@ -0,0 +1,402 @@
import { MournbladeUtility } from "../mournblade-utility.js"
/**
* Dialogue de jet de dé pour Mournblade - Version DialogV2
*/
export class MournbladeRollDialog {
/**
* Create and display the roll dialog
* @param {MournbladeActor} actor - The actor making the roll
* @param {Object} rollData - Data for the roll
* @returns {Promise<MournbladeRollDialog>}
*/
static async create(actor, rollData) {
// Préparer le contexte pour le template
const context = {
...rollData,
difficulte: String(rollData.difficulte || 0),
img: actor.img,
name: actor.name,
config: game.system.mournblade.config,
}
// Si attrKey est "tochoose", préparer la liste des attributs sélectionnables
if (rollData.attrKey === "tochoose") {
context.selectableAttributes = actor.system.attributs
}
// Rendre le template en HTML
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-mournblade/templates/roll-dialog-v2.hbs",
context
)
// Utiliser DialogV2.wait avec le HTML rendu
return foundry.applications.api.DialogV2.wait({
window: { title: "Test de Capacité", icon: "fa-solid fa-dice-d20" },
classes: ["mournblade-roll-dialog"],
position: { width: 500 },
modal: false,
content,
buttons: [
{
action: "rolld10",
label: "Lancer 1d10",
icon: "fa-solid fa-dice-d10",
default: true,
callback: async (event, button, dialog) => {
this._updateRollDataFromForm(rollData, button.form.elements, actor)
rollData.mainDice = "1d10"
await MournbladeUtility.rollMournblade(rollData)
}
},
{
action: "rolld20",
label: "Lancer 1d20",
icon: "fa-solid fa-dice-d20",
callback: async (event, button, dialog) => {
this._updateRollDataFromForm(rollData, button.form.elements, actor)
rollData.mainDice = "1d20"
await MournbladeUtility.rollMournblade(rollData)
}
},
],
rejectClose: false,
})
}
/**
* Create and display the sortilège (multi-rune spell) roll dialog
* @param {MournbladeActor} actor
* @param {Object} rollData
*/
static async createSortilege(actor, rollData) {
const ameDisponible = Math.max(1, (actor.system.ame.currentmax - actor.system.ame.value))
const context = {
...rollData,
img: actor.img,
name: actor.name,
config: game.system.mournblade.config,
runes: actor.getRunes(),
runemode: "prononcer",
ameDisponible,
ameOptions: Array.from({ length: ameDisponible }, (_, i) => i + 1),
modOptions: Array.from({ length: 21 }, (_, i) => i - 10),
}
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-mournblade/templates/dialog-sortilege.hbs",
context
)
// Attach dynamic recalculation after render
Hooks.once("renderDialogV2", (_app, html) => {
const form = html.querySelector ? html : html[0]
MournbladeRollDialog._attachSortilegeListeners(form)
})
return foundry.applications.api.DialogV2.wait({
window: { title: "Lancer un Sortilège", icon: "fa-solid fa-star-of-david" },
classes: ["mournblade-roll-dialog"],
position: { width: 520 },
modal: false,
content,
buttons: [
{
action: "rolld10",
label: "Lancer 1d10",
icon: "fa-solid fa-dice-d10",
default: true,
callback: async (event, button, dialog) => {
this._updateSortilegeRollData(rollData, button.form.elements, actor)
rollData.mainDice = "1d10"
await MournbladeUtility.rollSortilege(rollData)
}
},
{
action: "rolld20",
label: "Lancer 1d20",
icon: "fa-solid fa-dice-d20",
callback: async (event, button, dialog) => {
this._updateSortilegeRollData(rollData, button.form.elements, actor)
rollData.mainDice = "1d20"
await MournbladeUtility.rollSortilege(rollData)
}
},
],
rejectClose: false,
})
}
/**
* Open the potion preparation dialog
* @param {MournbladeActor} actor
* @param {object} rollData
*/
static async createPotion(actor, rollData) {
const ameDisponible = Math.max(1, (actor.system.ame.currentmax - actor.system.ame.value))
const context = {
...rollData,
img: actor.img,
name: actor.name,
config: game.system.mournblade.config,
runes: actor.getRunes(),
ameDisponible,
ameOptions: Array.from({ length: ameDisponible }, (_, i) => i + 1),
modOptions: Array.from({ length: 21 }, (_, i) => i - 10),
}
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-mournblade/templates/dialog-potion.hbs",
context
)
Hooks.once("renderDialogV2", (_app, html) => {
const form = html.querySelector ? html : html[0]
MournbladeRollDialog._attachPotionListeners(form)
})
return foundry.applications.api.DialogV2.wait({
window: { title: "Préparer une Potion", icon: "fa-solid fa-flask" },
classes: ["mournblade-roll-dialog"],
position: { width: 520 },
modal: false,
content,
buttons: [
{
action: "preparer",
label: "Préparer",
icon: "fa-solid fa-flask",
default: true,
callback: async (event, button, dialog) => {
MournbladeRollDialog._updatePotionRollData(rollData, button.form.elements, actor)
await MournbladeUtility.rollPotion(rollData)
}
},
],
rejectClose: false,
})
}
/**
* Attach dynamic recalculation listeners to the potion dialog
* @param {HTMLElement} html
*/
static _attachPotionListeners(html) {
const recalculate = () => {
const radioChecked = html.querySelector('.potion-rune-radio:checked')
const runeRow = radioChecked?.closest('[data-seuil]')
const seuil = parseInt(runeRow?.dataset.seuil ?? 0)
const pa = parseInt(html.querySelector('[name="pointsAme"]')?.value ?? 1)
const mod = parseInt(html.querySelector('[name="modificateur"]')?.value ?? 0)
const difficulteEl = html.querySelector('#potion-difficulte')
if (difficulteEl) difficulteEl.textContent = (seuil + pa) + (mod !== 0 ? ` (mod ${mod > 0 ? '+' : ''}${mod})` : '')
const tempsEl = html.querySelector('#potion-temps')
if (tempsEl) {
const heures = Math.max(1, Math.ceil(pa / 3))
tempsEl.textContent = heures === 1 ? '1 heure' : `${heures} heures`
}
}
html.querySelectorAll('.potion-rune-radio, [name="pointsAme"]').forEach(el => {
el.addEventListener('change', recalculate)
})
recalculate()
}
/**
* Update rollData from the potion dialog form elements
* @param {object} rollData
* @param {HTMLFormControlsCollection} formElements
* @param {MournbladeActor} actor
*/
static _updatePotionRollData(rollData, formElements, actor) {
const runeId = formElements['rune-selected']?.value
if (runeId) {
const rune = actor.getRunes().find(r => r._id === runeId)
if (rune) {
rollData.runeId = runeId
rollData.runeName = rune.name
rollData.runeImg = rune.img
rollData.runeSeuil = rune.system.seuil ?? 0
rollData.runeHautParler = rune.system.hautParler ?? ""
}
}
rollData.pointsAme = parseInt(formElements['pointsAme']?.value ?? 1)
rollData.forme = formElements['forme']?.value ?? "liquide"
rollData.modificateur = parseInt(formElements['modificateur']?.value ?? 0)
}
/**
* Attach dynamic recalculation listeners to the sortilège dialog
* @param {HTMLElement} html
*/
static _attachSortilegeListeners(html) {
const recalculate = () => {
const checkboxes = html.querySelectorAll('.sortilege-rune-checkbox:checked')
const modeSelect = html.querySelector('[name="runemode"]')
const mode = modeSelect?.value ?? "prononcer"
let maxSeuil = 0
let totalAme = 0
let totalActions = 0
let count = 0
// Enable/disable points inputs based on checkbox state
html.querySelectorAll('.sortilege-rune-checkbox').forEach(cb => {
const row = cb.closest('.sortilege-rune-row')
const pointsInput = row?.querySelector('.sortilege-rune-points')
if (pointsInput) pointsInput.disabled = !cb.checked
})
checkboxes.forEach(cb => {
const row = cb.closest('.sortilege-rune-row')
const seuil = Number(row?.dataset.seuil ?? 0)
const pts = Number(row?.querySelector('.sortilege-rune-points')?.value ?? 1)
if (seuil > maxSeuil) maxSeuil = seuil
totalAme += pts
totalActions += Math.ceil(pts / 3) * (mode === "inscrire" ? 2 : 1)
count++
})
const difficulte = count > 0 ? maxSeuil + (count - 1) : 0
html.querySelector('#sortilege-difficulte').textContent = count > 0 ? difficulte : '—'
html.querySelector('#sortilege-total-ame').textContent = totalAme
html.querySelector('#sortilege-actions').textContent = totalActions
}
html.querySelectorAll('.sortilege-rune-checkbox, .sortilege-rune-points, [name="runemode"]')
.forEach(el => el.addEventListener('change', recalculate))
}
/**
* Extract sortilège data from the form
* @param {Object} rollData
* @param {HTMLFormControlsCollection} formElements
* @param {MournbladeActor} actor
*/
static _updateSortilegeRollData(rollData, formElements, actor) {
rollData.runemode = formElements.runemode?.value ?? "prononcer"
rollData.modificateur = Number(formElements.modificateur?.value ?? 0)
rollData.runeautocible = formElements.runeautocible?.checked ?? false
// Collect selected runes with their soul points
const runes = actor.getRunes()
rollData.sortilegeRunes = []
for (const rune of runes) {
const checkbox = formElements[`rune-selected-${rune._id}`]
if (checkbox?.checked) {
const pts = Math.max(1, Number(formElements[`rune-points-${rune._id}`]?.value ?? 1))
rollData.sortilegeRunes.push({
id: rune._id,
name: rune.name,
img: rune.img,
seuil: rune.system.seuil,
formule: rune.system.formule,
pts,
})
}
}
if (rollData.sortilegeRunes.length === 0) return
const maxSeuil = Math.max(...rollData.sortilegeRunes.map(r => r.seuil))
rollData.difficulte = maxSeuil + (rollData.sortilegeRunes.length - 1)
rollData.runeame = rollData.sortilegeRunes.reduce((s, r) => s + r.pts, 0)
}
/**
* @param {Object} rollData - L'objet rollData à mettre à jour
* @param {HTMLFormControlsCollection} formElements - Les éléments du formulaire
* @param {MournbladeActor} actor - L'acteur pour récupérer les attributs
* @private
*/
static _updateRollDataFromForm(rollData, formElements, actor) {
// Attributs
if (formElements.attrKey) {
rollData.attrKey = formElements.attrKey.value
if (rollData.attrKey !== "tochoose" && actor) {
rollData.attr = foundry.utils.duplicate(actor.system.attributs[rollData.attrKey])
rollData.actionImg = "systems/fvtt-mournblade/assets/icons/" + actor.system.attributs[rollData.attrKey].labelnorm + ".webp"
}
}
// Modificateurs de base
if (formElements.difficulte) {
rollData.difficulte = Number(formElements.difficulte.value)
}
if (formElements.modificateur) {
rollData.modificateur = Number(formElements.modificateur.value)
}
// Runes
if (formElements.runemode) {
rollData.runemode = String(formElements.runemode.value)
}
if (formElements.runeame) {
rollData.runeame = Number(formElements.runeame.value)
}
if (formElements.runeautocible !== undefined) {
rollData.runeautocible = formElements.runeautocible.checked
}
// Combat mêlée
if (formElements.typeAttaque) {
rollData.typeAttaque = String(formElements.typeAttaque.value)
}
if (formElements.isMonte !== undefined) {
rollData.isMonte = formElements.isMonte.checked
}
// Combat distance
if (formElements.visee !== undefined) {
rollData.visee = formElements.visee.checked
}
if (formElements.cibleconsciente !== undefined) {
rollData.cibleconsciente = formElements.cibleconsciente.checked
}
if (formElements.ciblecourt !== undefined) {
rollData.ciblecourt = formElements.ciblecourt.checked
}
if (formElements.typeCouvert) {
rollData.typeCouvert = String(formElements.typeCouvert.value)
}
// Désavantages
if (!rollData.desavantages) rollData.desavantages = {}
if (formElements.cibleausol !== undefined) {
rollData.desavantages.cibleausol = formElements.cibleausol.checked
}
if (formElements.cibledesarmee !== undefined) {
rollData.desavantages.cibledesarmee = formElements.cibledesarmee.checked
}
if (formElements.ciblerestreint !== undefined) {
rollData.desavantages.ciblerestreint = formElements.ciblerestreint.checked
}
if (formElements.cibleimmobilisée !== undefined) {
rollData.desavantages.cibleimmobilisée = formElements.cibleimmobilisée.checked
}
if (formElements.ciblesurplomb !== undefined) {
rollData.desavantages.ciblesurplomb = formElements.ciblesurplomb.checked
}
// Double D20
if (formElements.doubleD20 !== undefined) {
rollData.doubleD20 = formElements.doubleD20.checked
}
// Modifiers
if (rollData.modifiers) {
rollData.modifiers.forEach((modifier, idx) => {
const checkbox = formElements[`apply-modifier-${idx}`]
if (checkbox) {
modifier.system.apply = checkbox.checked
}
})
}
}
}
+30
View File
@@ -0,0 +1,30 @@
/**
* Index des applications AppV2 pour Mournblade
* Ce fichier centralise tous les exports des applications
*/
// Applications de feuilles d'acteurs
export { default as MournbladePersonnageSheet } from './mournblade-personnage-sheet.mjs';
export { default as MournbladeCreatureSheet } from './mournblade-creature-sheet.mjs';
export { default as MournbladePnjSheet } from './mournblade-pnj-sheet.mjs';
// Applications de feuilles d'items
export { default as MournbladeArmeSheet } from './mournblade-arme-sheet.mjs';
export { default as MournbladeBouclierSheet } from './mournblade-bouclier-sheet.mjs';
export { default as MournbladeCapaciteSheet } from './mournblade-capacite-sheet.mjs';
export { default as MournbladeCompetenceSheet } from './mournblade-competence-sheet.mjs';
export { default as MournbladeDonSheet } from './mournblade-don-sheet.mjs';
export { default as MournbladeEquipementSheet } from './mournblade-equipement-sheet.mjs';
export { default as MournbladeHeritageSheet } from './mournblade-heritage-sheet.mjs';
export { default as MournbladeMetierSheet } from './mournblade-metier-sheet.mjs';
export { default as MournbladeModifierSheet } from './mournblade-modifier-sheet.mjs';
export { default as MournbladeMonnaieSheet } from './mournblade-monnaie-sheet.mjs';
export { default as MournbladeOrigineSheet } from './mournblade-origine-sheet.mjs';
export { default as MournbladePacteSheet } from './mournblade-pacte-sheet.mjs';
export { default as MournbladeProtectionSheet } from './mournblade-protection-sheet.mjs';
export { default as MournbladeRuneSheet } from './mournblade-rune-sheet.mjs';
export { default as MournbladeRuneEffectSheet } from './mournblade-runeeffect-sheet.mjs';
export { default as MournbladePotionSheet } from './mournblade-potion-sheet.mjs';
export { default as MournbladeTendanceSheet } from './mournblade-tendance-sheet.mjs';
export { default as MournbladeTraitChaotiqueSheet } from './mournblade-traitchaotique-sheet.mjs';
export { default as MournbladeTraitEspeceSheet } from './mournblade-traitespece-sheet.mjs';
@@ -0,0 +1,522 @@
const { HandlebarsApplicationMixin } = foundry.applications.api
import { MournbladeUtility } from "../../mournblade-utility.js"
export default class MournbladeActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) {
/**
* Different sheet modes.
* @enum {number}
*/
static SHEET_MODES = { EDIT: 0, PLAY: 1 }
constructor(options = {}) {
super(options)
this.#dragDrop = this.#createDragDropHandlers()
this._sheetMode = this.constructor.SHEET_MODES.PLAY // Commencer en mode visualisation
}
#dragDrop
/** @override */
static DEFAULT_OPTIONS = {
classes: ["fvtt-mournblade", "sheet", "actor"],
position: {
width: 650,
height: 720,
},
form: {
submitOnChange: true,
closeOnSubmit: false,
},
window: {
resizable: true,
},
tabs: [
{
navSelector: 'nav[data-group="primary"]',
contentSelector: "section.sheet-body",
initial: "stats",
},
],
dragDrop: [{ dragSelector: ".item-list .item", dropSelector: null }],
actions: {
editImage: MournbladeActorSheet.#onEditImage,
toggleSheet: MournbladeActorSheet.#onToggleSheet,
editItem: MournbladeActorSheet.#onEditItem,
deleteItem: MournbladeActorSheet.#onDeleteItem,
createItem: MournbladeActorSheet.#onCreateItem,
postItem: MournbladeActorSheet.#onPostItem,
equipItem: MournbladeActorSheet.#onEquipItem,
modifyQuantity: MournbladeActorSheet.#onModifyQuantity,
modifySante: MournbladeActorSheet.#onModifySante,
modifyAme: MournbladeActorSheet.#onModifyAme,
rollAttribut: MournbladeActorSheet.#onRollAttribut,
rollCompetence: MournbladeActorSheet.#onRollCompetence,
rollRune: MournbladeActorSheet.#onRollRune,
rollSortilege: MournbladeActorSheet.#onRollSortilege,
preparePotion: MournbladeActorSheet.#onPreparePotion,
invoquerElementaire: MournbladeActorSheet.#onInvoquerElementaire,
bannirElementaire: MournbladeActorSheet.#onBannirElementaire,
invoquerDemon: MournbladeActorSheet.#onInvoquerDemon,
libererDemon: MournbladeActorSheet.#onLibererDemon,
invoquerEsprit: MournbladeActorSheet.#onInvoquerEsprit,
enchanter: MournbladeActorSheet.#onEnchanter,
rollArmeOffensif: MournbladeActorSheet.#onRollArmeOffensif,
rollArmeSpecial: MournbladeActorSheet.#onRollArmeSpecial,
rollArmeDegats: MournbladeActorSheet.#onRollArmeDegats,
rollAssommer: MournbladeActorSheet.#onRollAssommer,
rollImmobiliser: MournbladeActorSheet.#onRollImmobiliser,
rollFuir: MournbladeActorSheet.#onRollFuir,
},
}
/**
* Is the sheet currently in 'Play' mode?
* @type {boolean}
*/
get isPlayMode() {
if (this._sheetMode === undefined) this._sheetMode = this.constructor.SHEET_MODES.PLAY
return this._sheetMode === this.constructor.SHEET_MODES.PLAY
}
/**
* Is the sheet currently in 'Edit' mode?
* @type {boolean}
*/
get isEditMode() {
if (this._sheetMode === undefined) this._sheetMode = this.constructor.SHEET_MODES.PLAY
return this._sheetMode === this.constructor.SHEET_MODES.EDIT
}
/**
* Tab groups state
* @type {object}
*/
tabGroups = { primary: "stats" }
/** @override */
async _prepareContext() {
const actor = this.document
const context = {
actor: actor,
system: actor.system,
source: actor.toObject(),
fields: actor.schema.fields,
systemFields: actor.system.schema.fields,
isEditable: this.isEditable,
isEditMode: this.isEditMode,
isPlayMode: this.isPlayMode,
isGM: game.user.isGM,
config: game.system.mournblade.config,
enrichedDescription: await foundry.applications.ux.TextEditor.implementation.enrichHTML(actor.system.biodata?.description || "", { async: true }),
}
return context
}
/** @override */
_preSyncPartState(partId, newElement, priorElement, state) {
super._preSyncPartState(partId, newElement, priorElement, state)
// Save scrollable tab positions for deferred restoration in _onRender.
// Tabs are hidden (display:none) at _syncPartState time, so scrollTop
// assignments have no effect. We re-apply them after making tabs visible.
const part = this.constructor.PARTS?.[partId]
if (part?.scrollable) {
this._pendingScrollRestores = part.scrollable.map(selector => {
const el = selector ? priorElement.querySelector(selector) : priorElement
return el ? { selector, scrollTop: el.scrollTop } : null
}).filter(Boolean)
}
}
/** @override */
_onRender(context, options) {
super._onRender(context, options)
this.#dragDrop.forEach((d) => d.bind(this.element))
// Handle edit-item-data changes
this.element.querySelectorAll('.edit-item-data').forEach(element => {
element.addEventListener('change', async (event) => {
const target = event.currentTarget
const itemElement = target.closest('[data-item-id]')
if (!itemElement) return
const itemId = itemElement.dataset.itemId
const itemType = itemElement.dataset.itemType
const itemField = target.dataset.itemField
const dataType = target.dataset.dtype
const value = target.value
await this.document.editItemField(itemId, itemType, itemField, dataType, value)
})
})
// Activate tab navigation manually
const nav = this.element.querySelector('nav.tabs[data-group]')
if (nav) {
const group = nav.dataset.group
const activeTab = this.tabGroups[group] || "stats"
const switchTab = (tab) => {
this.tabGroups[group] = tab
nav.querySelectorAll('[data-tab]').forEach(link => {
link.classList.toggle('active', link.dataset.tab === tab)
})
this.element.querySelectorAll('[data-group="' + group + '"][data-tab]').forEach(content => {
content.classList.toggle('active', content.dataset.tab === tab)
})
}
// Set initial state (makes active tab visible)
switchTab(activeTab)
// Restore scroll positions now that the active tab is visible
if (this._pendingScrollRestores?.length) {
for (const { selector, scrollTop } of this._pendingScrollRestores) {
const el = selector ? this.element.querySelector(selector) : this.element
if (el) el.scrollTop = scrollTop
}
this._pendingScrollRestores = null
}
// Tab clicks: DOM-only, no re-render (preserves scroll positions)
nav.querySelectorAll('[data-tab]').forEach(link => {
link.addEventListener('click', (event) => {
event.preventDefault()
switchTab(link.dataset.tab)
})
})
}
}
// #region Drag-and-Drop Workflow
/**
* Create drag-and-drop workflow handlers for this Application
*/
#createDragDropHandlers() {
return []
}
// #region Actions
/** @override */
static ACTIONS = {
editImage: MournbladeActorSheet.#onEditImage,
toggleSheet: MournbladeActorSheet.#onToggleSheet,
editItem: MournbladeActorSheet.#onEditItem,
deleteItem: MournbladeActorSheet.#onDeleteItem,
createItem: MournbladeActorSheet.#onCreateItem,
equipItem: MournbladeActorSheet.#onEquipItem,
modifyQuantity: MournbladeActorSheet.#onModifyQuantity,
rollAttribut: MournbladeActorSheet.#onRollAttribut,
rollCompetence: MournbladeActorSheet.#onRollCompetence,
rollArmeOffensif: MournbladeActorSheet.#onRollArmeOffensif,
rollArmeDegats: MournbladeActorSheet.#onRollArmeDegats,
rollAssommer: MournbladeActorSheet.#onRollAssommer,
rollImmobiliser: MournbladeActorSheet.#onRollImmobiliser,
rollFuir: MournbladeActorSheet.#onRollFuir,
}
/**
* Handle editing the actor image
* @param {Event} event - The triggering event
*/
static async #onEditImage(event) {
event.preventDefault()
const sheet = this
const filePicker = new FilePicker({
type: "image",
current: sheet.document.img,
callback: (path) => {
sheet.document.update({ img: path })
},
})
filePicker.browse()
}
/**
* Handle toggling the sheet mode
* @param {Event} event - The triggering event
*/
static async #onToggleSheet(event) {
event.preventDefault()
const sheet = this
sheet._sheetMode = sheet._sheetMode === sheet.constructor.SHEET_MODES.PLAY ? sheet.constructor.SHEET_MODES.EDIT : sheet.constructor.SHEET_MODES.PLAY
sheet.render()
}
/**
* Handle editing an item
* @param {Event} event - The triggering event
* @param {HTMLElement} target - The target element
*/
static async #onEditItem(event, target) {
const li = target.closest(".item")
const itemId = li?.dataset.itemId
if (!itemId) return
const item = this.actor.items.get(itemId)
if (item) item.sheet.render(true)
}
/**
* Handle deleting an item
* @param {Event} event - The triggering event
* @param {HTMLElement} target - The target element
*/
static async #onDeleteItem(event, target) {
const li = target.closest(".item")
await MournbladeUtility.confirmDelete(this, li)
}
/**
* Handle creating an item
* @param {Event} event - The triggering event
* @param {HTMLElement} target - The target element
*/
static async #onCreateItem(event, target) {
const itemType = target.dataset.type
await this.actor.createEmbeddedDocuments("Item", [{ name: `Nouveau ${itemType}`, type: itemType }], { renderSheet: true })
}
/**
* Handle posting an item to chat
*/
static async #onPostItem(event, target) {
const li = target.closest(".item")
const itemId = li?.dataset.itemId ?? target.dataset.itemId
if (!itemId) return
MournbladeUtility.postItemToChat(this.actor.id, itemId)
}
/**
* Handle equipping an item
* @param {Event} event - The triggering event
* @param {HTMLElement} target - The target element
*/
static async #onEquipItem(event, target) {
const li = target.closest(".item")
const itemId = li?.dataset.itemId
if (!itemId) return
const item = this.actor.items.get(itemId)
if (item) {
await item.update({ "system.equipped": !item.system.equipped })
}
}
/**
* Handle modifying the quantity of an item
* @param {Event} event - The triggering event
* @param {HTMLElement} target - The target element
*/
static async #onModifyQuantity(event, target) {
const li = target.closest('[data-item-id]')
const itemId = li?.dataset.itemId
const value = Number.parseInt(target.dataset.quantiteValue)
const item = this.document.items.get(itemId)
if (item) {
const newQuantity = Math.max(0, (item.system.quantite || 0) + value)
await item.update({ "system.quantite": newQuantity })
}
}
/**
* Handle modifying santé (health) values
* @param {Event} event - The triggering event
* @param {HTMLElement} target - The target element
*/
static async #onModifySante(event, target) {
const type = target.dataset.type
const value = Number.parseInt(target.dataset.value)
const actor = this.document
const currentValue = actor.system.sante[type] || 0
const newValue = Math.max(0, currentValue + value)
await actor.update({ [`system.sante.${type}`]: newValue })
}
/**
* Handle modifying âme (soul) value
* @param {Event} event - The triggering event
* @param {HTMLElement} target - The target element
*/
static async #onModifyAme(event, target) {
const value = Number.parseInt(target.dataset.value)
const actor = this.document
const currentValue = actor.system.ame.value || 0
const newValue = Math.max(0, currentValue + value)
await actor.update({ "system.ame.value": newValue })
}
/**
* Handle rolling an attribut
* @param {Event} event - The triggering event
*/
static async #onRollAttribut(event, target) {
event.preventDefault()
const sheet = this
const attrKey = target.dataset.attrKey
const actor = sheet.document
await actor.rollAttribut(attrKey)
}
/**
* Handle rolling a competence
* @param {Event} event - The triggering event
*/
static async #onRollCompetence(event, target) {
event.preventDefault()
const sheet = this
const attrKey = target.dataset.attrKey
const li = target.closest('[data-item-id]')
const compId = li?.dataset.itemId
const actor = sheet.document
await actor.rollCompetence(attrKey, compId)
}
/**
* Handle rolling a rune
* @param {Event} event - The triggering event
*/
static async #onRollRune(event, target) {
event.preventDefault()
const sheet = this
const li = target.closest('[data-item-id]')
const runeId = li?.dataset.itemId
const actor = sheet.document
await actor.rollRune(runeId)
}
/**
* Handle launching a sortilège (multi-rune spell)
*/
static async #onRollSortilege(event, target) {
event.preventDefault()
await this.document.rollSortilege()
}
/**
* Handle preparing a potion
*/
static async #onPreparePotion(event, target) {
event.preventDefault()
await this.document.preparePotion()
}
/**
* Handle invoking an elemental
*/
static async #onInvoquerElementaire(event, target) {
event.preventDefault()
await this.document.invoquerElementaire()
}
/**
* Handle banishing an elemental invocation
*/
static async #onBannirElementaire(event, target) {
event.preventDefault()
const invocIndex = parseInt(target.dataset.invocIndex ?? "0")
await this.document.bannirElementaire(invocIndex)
}
/**
* Handle invoking a demon
*/
static async #onInvoquerDemon(event, target) {
event.preventDefault()
await this.document.invoquerDemon()
}
/**
* Handle releasing a demon invocation
*/
static async #onLibererDemon(event, target) {
event.preventDefault()
const invocIndex = parseInt(target.dataset.invocIndex ?? "0")
await this.document.libererDemon(invocIndex)
}
/**
* Handle invoking a Law Spirit into an Automaton
*/
static async #onInvoquerEsprit(event, target) {
event.preventDefault()
await this.document.invoquerEspritLoi()
}
/**
* Handle enchanting an item with Loi power
*/
static async #onEnchanter(event, target) {
event.preventDefault()
const itemId = target.dataset.itemId
await this.document.enchanter(itemId)
}
/**
* Handle rolling an arme offensif
* @param {Event} event - The triggering event
*/
static async #onRollArmeOffensif(event, target) {
event.preventDefault()
const sheet = this
const armeId = target.dataset.armeId
const actor = sheet.document
await actor.rollArmeOffensif(armeId)
}
/**
* Handle rolling an arme degats
* @param {Event} event - The triggering event
*/
static async #onRollArmeDegats(event, target) {
event.preventDefault()
const sheet = this
const armeId = target.dataset.armeId
const actor = sheet.document
await actor.rollArmeDegats(armeId)
}
/**
* Handle rolling an arme special
* @param {Event} event - The triggering event
*/
static async #onRollArmeSpecial(event, target) {
event.preventDefault()
const sheet = this
const armeId = target.dataset.armeId
const actor = sheet.document
await actor.rollArmeSpecial(armeId)
}
/**
* Handle rolling an assommer
* @param {Event} event - The triggering event
*/
static async #onRollAssommer(event, target) {
event.preventDefault()
const sheet = this
const actor = sheet.document
await actor.rollAssomer()
}
/**
* Handle rolling an immobiliser
* @param {Event} event - The triggering event
*/
static async #onRollImmobiliser(event, target) {
event.preventDefault()
const sheet = this
const actor = sheet.document
await actor.rollImmobiliser()
}
/**
* Handle rolling a fuir
* @param {Event} event - The triggering event
*/
static async #onRollFuir(event, target) {
event.preventDefault()
const sheet = this
const actor = sheet.document
await actor.rollFuir()
}
}
@@ -0,0 +1,200 @@
const { HandlebarsApplicationMixin } = foundry.applications.api
export default class MournbladeItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) {
constructor(options = {}) {
super(options)
this.#dragDrop = this.#createDragDropHandlers()
}
#dragDrop
/** @override */
static DEFAULT_OPTIONS = {
classes: ["fvtt-mournblade", "item"],
position: {
width: 620,
height: 600,
},
form: {
submitOnChange: true,
},
window: {
resizable: true,
},
tabs: [
{
navSelector: 'nav[data-group="primary"]',
contentSelector: "section.sheet-body",
initial: "description",
},
],
dragDrop: [{ dragSelector: "[data-drag]", dropSelector: null }],
actions: {
editImage: MournbladeItemSheet.#onEditImage,
postItem: MournbladeItemSheet.#onPostItem,
enchanter: MournbladeItemSheet.#onEnchanter,
},
}
/**
* Tab groups state
* @type {object}
*/
tabGroups = { primary: "description" }
/** @override */
async _prepareContext() {
const item = this.document
const enchantableTypes = ["arme", "equipement", "protection", "bouclier"]
const actorIsLoyal = item.actor?.getAlignement?.() === "loyal"
const alreadyEnchanted = enchantableTypes.includes(item.type) && (item.system.enchantementLoi?.actif ?? false)
const canEnchant = enchantableTypes.includes(item.type) && !!item.actor && actorIsLoyal && !alreadyEnchanted
const context = {
fields: item.schema.fields,
systemFields: item.system.schema.fields,
item,
system: item.system,
source: item.toObject(),
enrichedDescription: await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.system.description, { async: true }),
isEditMode: true,
isEditable: this.isEditable,
isGM: game.user.isGM,
config: game.system.mournblade.config,
canEnchant,
enchantementActif: alreadyEnchanted,
}
return context
}
/** @override */
_onRender(context, options) {
super._onRender(context, options)
this.#dragDrop.forEach((d) => d.bind(this.element))
// Activate tab navigation manually
const nav = this.element.querySelector('nav.tabs[data-group]')
if (nav) {
const group = nav.dataset.group
// Activate the current tab
const activeTab = this.tabGroups[group] || "description"
nav.querySelectorAll('[data-tab]').forEach(link => {
const tab = link.dataset.tab
link.classList.toggle('active', tab === activeTab)
link.addEventListener('click', (event) => {
event.preventDefault()
this.tabGroups[group] = tab
this.render()
})
})
// Show/hide tab content
this.element.querySelectorAll('[data-group="' + group + '"][data-tab]').forEach(content => {
content.classList.toggle('active', content.dataset.tab === activeTab)
})
}
}
// #region Drag-and-Drop Workflow
/**
* Create drag-and-drop workflow handlers for this Application
*/
#createDragDropHandlers() {
return []
}
// #region Actions
/**
* Handle enchanting this item with Loi power
*/
static async #onEnchanter(event) {
event.preventDefault()
const item = this.document
if (!item.actor) {
ui.notifications.warn("Cet objet doit être sur un personnage pour être enchanté.")
return
}
await item.actor.enchanter(item.id)
}
/**
* Handle editing the item image
* @param {Event} event - The triggering event
*/
static async #onEditImage(event) {
event.preventDefault()
const filePicker = new FilePicker({
type: "image",
current: this.document.img,
callback: (path) => {
this.document.update({ img: path })
},
})
filePicker.browse()
}
/**
* Handle posting the item to chat
* @param {Event} event - The triggering event
*/
static async #onPostItem(event) {
event.preventDefault()
let chatData = foundry.utils.duplicate(this.document)
if (this.document.actor) {
chatData.actor = { id: this.document.actor.id }
}
if (chatData.img?.includes("/blank.png")) {
chatData.img = null
}
chatData.jsondata = JSON.stringify({
compendium: "postedItem",
payload: chatData,
})
// Localized type label
const typeLabels = {
arme: "Arme", bouclier: "Bouclier", competence: "Compétence",
rune: "Rune", runeeffect: "Rune Active", don: "Don", pacte: "Pacte",
protection: "Protection", equipement: "Équipement", heritage: "Héritage",
metier: "Métier", capacite: "Capacité", tendance: "Tendance",
traitchaotique: "Trait Chaotique", traitespece: "Trait d'Espèce",
origine: "Origine", modifier: "Modificateur", monnaie: "Monnaie",
potion: "Potion"
}
chatData.typeLabel = typeLabels[chatData.type] ?? chatData.type
// Type icon for the badge
const typeIcons = {
arme: "fa-sword", bouclier: "fa-shield-halved", competence: "fa-graduation-cap",
rune: "fa-star-of-david", runeeffect: "fa-star-of-david", don: "fa-hand-sparkles",
pacte: "fa-scroll", protection: "fa-shield", equipement: "fa-box",
heritage: "fa-dna", metier: "fa-hammer", capacite: "fa-bolt",
tendance: "fa-yin-yang", traitchaotique: "fa-skull", traitespece: "fa-paw",
origine: "fa-compass", modifier: "fa-sliders", monnaie: "fa-coins",
potion: "fa-flask"
}
chatData.typeIcon = typeIcons[chatData.type] ?? "fa-cube"
// Potion: add localized labels for statut and forme
if (chatData.type === "potion") {
const statutLabels = {
inconnue: game.i18n.localize("MNBL.potionInconnue"),
efficace: game.i18n.localize("MNBL.potionEfficace"),
heroique: game.i18n.localize("MNBL.potionHeroique"),
inefficace: game.i18n.localize("MNBL.potionInefficace"),
poison: game.i18n.localize("MNBL.potionPoison"),
}
const formeLabels = {
liquide: game.i18n.localize("MNBL.potionLiquide"),
onguent: game.i18n.localize("MNBL.potionOnguent"),
cachets: game.i18n.localize("MNBL.potionCachets"),
pilules: game.i18n.localize("MNBL.potionPilules"),
}
chatData.system.statutLabel = statutLabels[chatData.system.statut] ?? chatData.system.statut
chatData.system.formeLabel = formeLabels[chatData.system.forme] ?? chatData.system.forme
}
const html = await foundry.applications.handlebars.renderTemplate('systems/fvtt-mournblade/templates/post-item.hbs', chatData)
ChatMessage.create({ user: game.user.id, content: html })
}
}
@@ -0,0 +1,49 @@
import MournbladeItemSheet from "./base-item-sheet.mjs"
export default class MournbladeArmeSheet extends MournbladeItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["arme"],
position: {
width: 620,
},
window: {
contentClasses: ["arme-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-mournblade/templates/item-arme-sheet.hbs",
},
}
/** @override */
tabGroups = {
primary: "description",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
}
@@ -0,0 +1,49 @@
import MournbladeItemSheet from "./base-item-sheet.mjs"
export default class MournbladeBouclierSheet extends MournbladeItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["bouclier"],
position: {
width: 620,
},
window: {
contentClasses: ["bouclier-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-mournblade/templates/item-bouclier-sheet.hbs",
},
}
/** @override */
tabGroups = {
primary: "description",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
}
@@ -0,0 +1,49 @@
import MournbladeItemSheet from "./base-item-sheet.mjs"
export default class MournbladeCapaciteSheet extends MournbladeItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["capacite"],
position: {
width: 620,
},
window: {
contentClasses: ["capacite-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-mournblade/templates/item-capacite-sheet.hbs",
},
}
/** @override */
tabGroups = {
primary: "description",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
}
@@ -0,0 +1,78 @@
import MournbladeItemSheet from "./base-item-sheet.mjs"
export default class MournbladeCompetenceSheet extends MournbladeItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["competence"],
position: {
width: 620,
},
window: {
contentClasses: ["competence-content"],
},
actions: {
...MournbladeItemSheet.DEFAULT_OPTIONS.actions,
addPredilection: MournbladeCompetenceSheet.#onAddPredilection,
deletePredilection: MournbladeCompetenceSheet.#onDeletePredilection,
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-mournblade/templates/item-competence-sheet.hbs",
},
}
/** @override */
tabGroups = {
primary: "description",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
/**
* Add a new empty predilection to the competence
* @param {PointerEvent} event
*/
static async #onAddPredilection(event) {
event.preventDefault()
const predilections = foundry.utils.duplicate(this.document.system.predilections ?? [])
predilections.push({ name: "", used: false })
await this.document.update({ "system.predilections": predilections })
}
/**
* Delete a predilection by index
* @param {PointerEvent} event
* @param {HTMLElement} target
*/
static async #onDeletePredilection(event, target) {
event.preventDefault()
const idx = Number(target.dataset.predictionIndex)
const predilections = foundry.utils.duplicate(this.document.system.predilections ?? [])
predilections.splice(idx, 1)
await this.document.update({ "system.predilections": predilections })
}
}
@@ -0,0 +1,45 @@
import MournbladeActorSheet from "./base-actor-sheet.mjs"
export default class MournbladeCreatureSheet extends MournbladeActorSheet {
/** @override */
static DEFAULT_OPTIONS = {
...super.DEFAULT_OPTIONS,
classes: [...super.DEFAULT_OPTIONS.classes],
window: {
...super.DEFAULT_OPTIONS.window,
title: "SHEETS.Actor.creature",
},
}
/** @override */
static PARTS = {
sheet: {
template: "systems/fvtt-mournblade/templates/creature-sheet.hbs",
scrollable: [".tab.competences", ".tab.equipement", ".tab.biodata"],
},
}
/** @override */
tabGroups = {
primary: "stats",
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
const actor = this.document
// Add creature-specific data
context.skills = actor.getSkills()
context.armes = foundry.utils.duplicate(actor.getWeapons())
context.protections = foundry.utils.duplicate(actor.getArmors())
context.capacites = foundry.utils.duplicate(actor.getCapacites())
context.runes = foundry.utils.duplicate(actor.getRunes())
context.combat = actor.getCombatValues()
context.equipements = foundry.utils.duplicate(actor.getEquipments())
context.protectionTotal = actor.getProtectionTotal()
context.santeMalus = actor.getStatusMalus()
context.ameMalus = actor.getAmeMalus()
return context
}
}
@@ -0,0 +1,50 @@
import MournbladeItemSheet from "./base-item-sheet.mjs"
export default class MournbladeDonSheet extends MournbladeItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["don"],
position: {
width: 620,
},
window: {
contentClasses: ["don-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-mournblade/templates/item-don-sheet.hbs",
},
}
/** @override */
tabGroups = {
primary: "description",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
context.enrichedSacrifice = await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.sacrifice, { async: true })
return context
}
}
@@ -0,0 +1,49 @@
import MournbladeItemSheet from "./base-item-sheet.mjs"
export default class MournbladeEquipementSheet extends MournbladeItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["equipement"],
position: {
width: 620,
},
window: {
contentClasses: ["equipement-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-mournblade/templates/item-equipement-sheet.hbs",
},
}
/** @override */
tabGroups = {
primary: "description",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
}
@@ -0,0 +1,49 @@
import MournbladeItemSheet from "./base-item-sheet.mjs"
export default class MournbladeHeritageSheet extends MournbladeItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["heritage"],
position: {
width: 620,
},
window: {
contentClasses: ["heritage-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-mournblade/templates/item-heritage-sheet.hbs",
},
}
/** @override */
tabGroups = {
primary: "description",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
}
@@ -0,0 +1,49 @@
import MournbladeItemSheet from "./base-item-sheet.mjs"
export default class MournbladeMetierSheet extends MournbladeItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["metier"],
position: {
width: 620,
},
window: {
contentClasses: ["metier-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-mournblade/templates/item-metier-sheet.hbs",
},
}
/** @override */
tabGroups = {
primary: "description",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
}
@@ -0,0 +1,49 @@
import MournbladeItemSheet from "./base-item-sheet.mjs"
export default class MournbladeModifierSheet extends MournbladeItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["modifier"],
position: {
width: 620,
},
window: {
contentClasses: ["modifier-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-mournblade/templates/item-modifier-sheet.hbs",
},
}
/** @override */
tabGroups = {
primary: "description",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
}
@@ -0,0 +1,49 @@
import MournbladeItemSheet from "./base-item-sheet.mjs"
export default class MournbladeMonnaieSheet extends MournbladeItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["monnaie"],
position: {
width: 620,
},
window: {
contentClasses: ["monnaie-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-mournblade/templates/item-monnaie-sheet.hbs",
},
}
/** @override */
tabGroups = {
primary: "description",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
}
@@ -0,0 +1,49 @@
import MournbladeItemSheet from "./base-item-sheet.mjs"
export default class MournbladeOrigineSheet extends MournbladeItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["origine"],
position: {
width: 620,
},
window: {
contentClasses: ["origine-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-mournblade/templates/item-origine-sheet.hbs",
},
}
/** @override */
tabGroups = {
primary: "description",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
}
@@ -0,0 +1,49 @@
import MournbladeItemSheet from "./base-item-sheet.mjs"
export default class MournbladePacteSheet extends MournbladeItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["pacte"],
position: {
width: 620,
},
window: {
contentClasses: ["pacte-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-mournblade/templates/item-pacte-sheet.hbs",
},
}
/** @override */
tabGroups = {
primary: "description",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
}
@@ -0,0 +1,61 @@
import MournbladeActorSheet from "./base-actor-sheet.mjs"
export default class MournbladePersonnageSheet extends MournbladeActorSheet {
/** @override */
static DEFAULT_OPTIONS = {
...super.DEFAULT_OPTIONS,
classes: [...super.DEFAULT_OPTIONS.classes],
window: {
...super.DEFAULT_OPTIONS.window,
title: "SHEETS.Actor.personnage",
},
}
/** @override */
static PARTS = {
sheet: {
template: "systems/fvtt-mournblade/templates/actor-sheet.hbs",
scrollable: [".tab.principal", ".tab.competences", ".tab.dons", ".tab.equipement"],
},
}
/** @override */
tabGroups = {
primary: "principal",
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
const actor = this.document
// Add personnage-specific data
context.skills = actor.getSkills()
context.armes = foundry.utils.duplicate(actor.getWeapons())
context.protections = foundry.utils.duplicate(actor.getArmors())
context.dons = foundry.utils.duplicate(actor.getDons())
context.pactes = foundry.utils.duplicate(actor.getPactes())
context.alignement = actor.getAlignement()
context.isChaotique = context.alignement === "chaotique"
context.isLoyal = context.alignement === "loyal"
context.aspect = actor.getAspect()
context.marge = actor.getMarge()
context.tendances = foundry.utils.duplicate(actor.getTendances())
context.runes = foundry.utils.duplicate(actor.getRunes())
context.traitsChaotiques = foundry.utils.duplicate(actor.getTraitsChaotiques())
context.traitsEspeces = foundry.utils.duplicate(actor.getTraitsEspeces())
context.origine = foundry.utils.duplicate(actor.getOrigine() || {})
context.heritage = foundry.utils.duplicate(actor.getHeritage() || {})
context.metier = foundry.utils.duplicate(actor.getMetier() || {})
context.combat = actor.getCombatValues()
context.equipements = foundry.utils.duplicate(actor.getEquipments())
context.potions = foundry.utils.duplicate(actor.getPotions())
context.modifiers = foundry.utils.duplicate(actor.getModifiers())
context.monnaies = foundry.utils.duplicate(actor.getMonnaies())
context.runeEffects = foundry.utils.duplicate(actor.getRuneEffects())
context.protectionTotal = actor.getProtectionTotal()
context.santeMalus = actor.getStatusMalus()
context.ameMalus = actor.getAmeMalus()
return context
}
}
@@ -0,0 +1,47 @@
/**
* Application de feuille de PNJ pour Mournblade
*/
import MournbladeActorSheet from "./base-actor-sheet.mjs"
export default class MournbladePnjSheet extends MournbladeActorSheet {
/** @override */
static DEFAULT_OPTIONS = {
...super.DEFAULT_OPTIONS,
classes: [...super.DEFAULT_OPTIONS.classes],
window: {
...super.DEFAULT_OPTIONS.window,
title: "SHEETS.Actor.pnj",
},
}
/** @override */
static PARTS = {
sheet: {
template: "systems/fvtt-mournblade/templates/actor-sheet.hbs",
},
}
/** @override */
tabGroups = {
primary: "principal",
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
const actor = this.document
// Add pnj-specific data
context.skills = actor.getSkills()
context.armes = foundry.utils.duplicate(actor.getWeapons())
context.protections = foundry.utils.duplicate(actor.getArmors())
context.dons = foundry.utils.duplicate(actor.getDons())
context.pactes = foundry.utils.duplicate(actor.getPactes())
context.combat = actor.getCombatValues()
context.equipements = foundry.utils.duplicate(actor.getEquipments())
context.protectionTotal = actor.getProtectionTotal()
context.santeMalus = actor.getStatusMalus()
context.ameMalus = actor.getAmeMalus()
return context
}
}
@@ -0,0 +1,47 @@
import MournbladeItemSheet from "./base-item-sheet.mjs"
export default class MournbladePotionSheet extends MournbladeItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["potion"],
position: {
width: 640,
},
window: {
contentClasses: ["potion-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-mournblade/templates/item-potion-sheet.hbs",
},
}
/** @override */
tabGroups = {
primary: "details",
}
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
effets: { id: "effets", group: "primary", label: "Effets" },
description: { id: "description", group: "primary", label: "Description" },
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
context.config = game.system.mournblade.config
return context
}
}
@@ -0,0 +1,49 @@
import MournbladeItemSheet from "./base-item-sheet.mjs"
export default class MournbladeProtectionSheet extends MournbladeItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["protection"],
position: {
width: 620,
},
window: {
contentClasses: ["protection-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-mournblade/templates/item-protection-sheet.hbs",
},
}
/** @override */
tabGroups = {
primary: "description",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
}
@@ -0,0 +1,49 @@
import MournbladeItemSheet from "./base-item-sheet.mjs"
export default class MournbladeRuneSheet extends MournbladeItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["rune"],
position: {
width: 620,
},
window: {
contentClasses: ["rune-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-mournblade/templates/item-rune-sheet.hbs",
},
}
/** @override */
tabGroups = {
primary: "description",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
}
@@ -0,0 +1,49 @@
import MournbladeItemSheet from "./base-item-sheet.mjs"
export default class MournbladeRuneEffectSheet extends MournbladeItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["runeeffect"],
position: {
width: 620,
},
window: {
contentClasses: ["runeeffect-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-mournblade/templates/item-runeeffect-sheet.hbs",
},
}
/** @override */
tabGroups = {
primary: "description",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
}
@@ -0,0 +1,49 @@
import MournbladeItemSheet from "./base-item-sheet.mjs"
export default class MournbladeTendanceSheet extends MournbladeItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["tendance"],
position: {
width: 620,
},
window: {
contentClasses: ["tendance-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-mournblade/templates/item-tendance-sheet.hbs",
},
}
/** @override */
tabGroups = {
primary: "description",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
}
@@ -0,0 +1,49 @@
import MournbladeItemSheet from "./base-item-sheet.mjs"
export default class MournbladeTraitChaotiqueSheet extends MournbladeItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["traitchaotique"],
position: {
width: 620,
},
window: {
contentClasses: ["traitchaotique-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-mournblade/templates/item-traitchaotique-sheet.hbs",
},
}
/** @override */
tabGroups = {
primary: "description",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
}
@@ -0,0 +1,49 @@
import MournbladeItemSheet from "./base-item-sheet.mjs"
export default class MournbladeTraitEspeceSheet extends MournbladeItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["traitespece"],
position: {
width: 620,
},
window: {
contentClasses: ["traitespece-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-mournblade/templates/item-traitespece-sheet.hbs",
},
}
/** @override */
tabGroups = {
primary: "description",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
details: { id: "details", group: "primary", label: "Détails" },
description: { id: "description", group: "primary", label: "Description" }
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
}
+32
View File
@@ -0,0 +1,32 @@
/**
* Data model pour les armes
*/
export default class ArmeDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" }),
typearme: new fields.StringField({ initial: "" }),
isdefense: new fields.BooleanField({ initial: false }),
bonusmaniementoff: new fields.NumberField({ initial: 0, integer: true }),
bonusmaniementdef: new fields.NumberField({ initial: 0, integer: true }),
ignorearmure: new fields.BooleanField({ initial: false }),
nbressources: new fields.NumberField({ initial: 0, integer: true }),
degats: new fields.StringField({ initial: "" }),
nonletaux: new fields.BooleanField({ initial: false }),
deuxmains: new fields.BooleanField({ initial: false }),
courte: new fields.NumberField({ initial: 0, integer: true }),
moyenne: new fields.NumberField({ initial: 0, integer: true }),
longue: new fields.NumberField({ initial: 0, integer: true }),
tr: new fields.NumberField({ initial: 0, integer: true }),
rarete: new fields.NumberField({ initial: 0, integer: true }),
prix: new fields.NumberField({ initial: 0, integer: true }),
equipped: new fields.BooleanField({ initial: false }),
enchantementLoi: new fields.SchemaField({
actif: new fields.BooleanField({ initial: false }),
bonus: new fields.NumberField({ initial: 0, integer: true }),
antiChaos: new fields.BooleanField({ initial: false }),
}),
};
}
}
+11
View File
@@ -0,0 +1,11 @@
/**
* Data model de base pour les items Mournblade
*/
export default class BaseItemDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" })
};
}
}
+22
View File
@@ -0,0 +1,22 @@
/**
* Data model pour les boucliers
*/
export default class BouclierDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" }),
bonusdefense: new fields.NumberField({ initial: 0, integer: true }),
degats: new fields.StringField({ initial: "" }),
nonletaux: new fields.BooleanField({ initial: false }),
rarete: new fields.NumberField({ initial: 0, integer: true }),
prix: new fields.NumberField({ initial: 0, integer: true }),
equipped: new fields.BooleanField({ initial: false }),
enchantementLoi: new fields.SchemaField({
actif: new fields.BooleanField({ initial: false }),
bonus: new fields.NumberField({ initial: 0, integer: true }),
antiChaos: new fields.BooleanField({ initial: false }),
}),
};
}
}
+12
View File
@@ -0,0 +1,12 @@
/**
* Data model pour les capacités
*/
export default class CapaciteDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
typeCapacite: new fields.StringField({ initial: "creature", blank: false }),
description: new fields.HTMLField({ initial: "" })
};
}
}
+32
View File
@@ -0,0 +1,32 @@
/**
* Data model pour les compétences
*/
export default class CompetenceDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" }),
niveau: new fields.NumberField({ initial: 0, integer: true }),
attribut1: new fields.StringField({ initial: "" }),
attribut2: new fields.StringField({ initial: "" }),
attribut3: new fields.StringField({ initial: "" }),
doublebonus: new fields.BooleanField({ initial: false }),
predilections: new fields.ArrayField(
new fields.SchemaField({
name: new fields.StringField({ initial: "" }),
used: new fields.BooleanField({ initial: false })
}),
{ initial: [] }
)
};
}
static migrateData(source) {
if (Array.isArray(source.predilections)) {
source.predilections = source.predilections.map(pred =>
typeof pred === "string" ? { name: pred, used: false } : pred
);
}
return super.migrateData(source);
}
}
+110
View File
@@ -0,0 +1,110 @@
/**
* Data model pour les créatures
*/
export default class CreatureDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
// Template biodata
biodata: new fields.SchemaField({
name: new fields.StringField({ initial: "" }),
age: new fields.NumberField({ initial: 0, integer: true }),
alignement: new fields.StringField({ initial: "" }),
creatureType: new fields.StringField({ initial: "creature" }),
elementType: new fields.StringField({ initial: "" }),
demonType: new fields.StringField({ initial: "" }),
demonPuissance: new fields.StringField({ initial: "" }),
automatonType: new fields.StringField({ initial: "" }),
automatonPuissance: new fields.StringField({ initial: "" }),
automatonVoyageType: new fields.StringField({ initial: "" }),
poids: new fields.StringField({ initial: "" }),
taille: new fields.StringField({ initial: "" }),
cheveux: new fields.StringField({ initial: "" }),
sexe: new fields.StringField({ initial: "" }),
yeux: new fields.StringField({ initial: "" }),
description: new fields.HTMLField({ initial: "" }),
amemultiplier: new fields.NumberField({ initial: 2, integer: true }),
ignoreamemalus: new fields.BooleanField({ initial: false }),
ignoresantemalus: new fields.BooleanField({ initial: false }),
notes: new fields.HTMLField({ initial: "" }),
gmnotes: new fields.HTMLField({ initial: "" })
}),
// Template core
subactors: new fields.ArrayField(new fields.StringField(), { initial: [] }),
attributs: new fields.SchemaField({
adr: new fields.SchemaField({
label: new fields.StringField({ initial: "Adresse" }),
labelnorm: new fields.StringField({ initial: "adresse" }),
abbrev: new fields.StringField({ initial: "adr" }),
value: new fields.NumberField({ initial: 1, integer: true })
}),
pui: new fields.SchemaField({
label: new fields.StringField({ initial: "Puissance" }),
labelnorm: new fields.StringField({ initial: "puissance" }),
abbrev: new fields.StringField({ initial: "pui" }),
value: new fields.NumberField({ initial: 1, integer: true })
}),
cla: new fields.SchemaField({
label: new fields.StringField({ initial: "Clairvoyance" }),
labelnorm: new fields.StringField({ initial: "clairvoyance" }),
abbrev: new fields.StringField({ initial: "cla" }),
value: new fields.NumberField({ initial: 1, integer: true })
}),
pre: new fields.SchemaField({
label: new fields.StringField({ initial: "Présence" }),
labelnorm: new fields.StringField({ initial: "presence" }),
abbrev: new fields.StringField({ initial: "pre" }),
value: new fields.NumberField({ initial: 0, integer: true })
}),
tre: new fields.SchemaField({
label: new fields.StringField({ initial: "Trempe" }),
labelnorm: new fields.StringField({ initial: "trempe" }),
abbrev: new fields.StringField({ initial: "tre" }),
value: new fields.NumberField({ initial: 0, integer: true })
})
}),
bonneaventure: new fields.SchemaField({
base: new fields.NumberField({ initial: 0, integer: true }),
actuelle: new fields.NumberField({ initial: 0, integer: true })
}),
experience: new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true })
}),
eclat: new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true })
}),
sante: new fields.SchemaField({
base: new fields.NumberField({ initial: 0, integer: true }),
bonus: new fields.NumberField({ initial: 0, integer: true }),
nonletaux: new fields.NumberField({ initial: 0, integer: true }),
letaux: new fields.NumberField({ initial: 0, integer: true }),
malusmanuel: new fields.NumberField({ initial: 0, integer: true }),
sequelles: new fields.StringField({ initial: "" })
}),
ame: new fields.SchemaField({
fullmax: new fields.NumberField({ initial: 0, integer: true }),
currentmax: new fields.NumberField({ initial: 0, integer: true }),
value: new fields.NumberField({ initial: 0, integer: true }),
traumatismes: new fields.StringField({ initial: "" })
}),
combat: new fields.SchemaField({
initbonus: new fields.NumberField({ initial: 0, integer: true }),
vitessebonus: new fields.NumberField({ initial: 0, integer: true }),
bonusdegats: new fields.NumberField({ initial: 0, integer: true }),
defensebonus: new fields.NumberField({ initial: 0, integer: true }),
monte: new fields.BooleanField({ initial: false })
}),
balance: new fields.SchemaField({
loi: new fields.NumberField({ initial: 0, integer: true }),
chaos: new fields.NumberField({ initial: 0, integer: true }),
aspect: new fields.NumberField({ initial: 0, integer: true }),
marge: new fields.NumberField({ initial: 0, integer: true }),
pointschaos: new fields.NumberField({ initial: 0, integer: true }),
pointsloi: new fields.NumberField({ initial: 0, integer: true })
}),
ressources: new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true })
})
};
}
}
+14
View File
@@ -0,0 +1,14 @@
/**
* Data model pour les dons
*/
export default class DonDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" }),
allegeance: new fields.StringField({ initial: "" }),
prerequis: new fields.StringField({ initial: "" }),
sacrifice: new fields.HTMLField({ initial: "" })
};
}
}
+18
View File
@@ -0,0 +1,18 @@
/**
* Data model pour les équipements
*/
export default class EquipementDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" }),
rarete: new fields.NumberField({ initial: 0, integer: true }),
prix: new fields.NumberField({ initial: 0, integer: true }),
enchantementLoi: new fields.SchemaField({
actif: new fields.BooleanField({ initial: false }),
bonus: new fields.NumberField({ initial: 0, integer: true }),
antiChaos: new fields.BooleanField({ initial: false }),
}),
};
}
}
+11
View File
@@ -0,0 +1,11 @@
/**
* Data model pour les héritages
*/
export default class HeritageDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" })
};
}
}
+29
View File
@@ -0,0 +1,29 @@
/**
* Index des DataModels pour Mournblade
* Ce fichier centralise tous les exports des modèles de données
*/
// Modèles d'items
export { default as ArmeDataModel } from './arme.mjs';
export { default as BouclierDataModel } from './bouclier.mjs';
export { default as CapaciteDataModel } from './capacite.mjs';
export { default as CompetenceDataModel } from './competence.mjs';
export { default as DonDataModel } from './don.mjs';
export { default as EquipementDataModel } from './equipement.mjs';
export { default as HeritageDataModel } from './heritage.mjs';
export { default as MetierDataModel } from './metier.mjs';
export { default as ModifierDataModel } from './modifier.mjs';
export { default as MonnaieDataModel } from './monnaie.mjs';
export { default as OrigineDataModel } from './origine.mjs';
export { default as PacteDataModel } from './pacte.mjs';
export { default as ProtectionDataModel } from './protection.mjs';
export { default as RuneDataModel } from './rune.mjs';
export { default as RuneEffectDataModel } from './runeeffect.mjs';
export { default as PotionDataModel } from './potion.mjs';
export { default as TendanceDataModel } from './tendance.mjs';
export { default as TraitChaotiqueDataModel } from './traitchaotique.mjs';
export { default as TraitEspeceDataModel } from './traitespece.mjs';
// Modèles d'acteurs
export { default as PersonnageDataModel } from './personnage.mjs';
export { default as CreatureDataModel } from './creature.mjs';
+11
View File
@@ -0,0 +1,11 @@
/**
* Data model pour les métiers
*/
export default class MetierDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" })
};
}
}
+18
View File
@@ -0,0 +1,18 @@
/**
* Data model pour les modificateurs
*/
export default class ModifierDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" }),
modifiertype: new fields.StringField({ initial: "roll" }),
value: new fields.NumberField({ initial: 0, integer: true }),
attribut: new fields.StringField({ initial: "aucun" }),
competence: new fields.StringField({ initial: "aucun" }),
permanent: new fields.BooleanField({ initial: true }),
once: new fields.BooleanField({ initial: false }),
duree: new fields.StringField({ initial: "" })
};
}
}
+13
View File
@@ -0,0 +1,13 @@
/**
* Data model pour les monnaies
*/
export default class MonnaieDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" }),
quantite: new fields.NumberField({ initial: 0, integer: true }),
unite: new fields.StringField({ initial: "" })
};
}
}
+11
View File
@@ -0,0 +1,11 @@
/**
* Data model pour les origines
*/
export default class OrigineDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" })
};
}
}
+12
View File
@@ -0,0 +1,12 @@
/**
* Data model pour les pactes
*/
export default class PacteDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" }),
allegeance: new fields.StringField({ initial: "" })
};
}
}
+102
View File
@@ -0,0 +1,102 @@
/**
* Data model pour les personnages
*/
export default class PersonnageDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
// Template biodata
biodata: new fields.SchemaField({
name: new fields.StringField({ initial: "" }),
age: new fields.NumberField({ initial: 0, integer: true }),
alignement: new fields.StringField({ initial: "" }),
poids: new fields.StringField({ initial: "" }),
taille: new fields.StringField({ initial: "" }),
cheveux: new fields.StringField({ initial: "" }),
sexe: new fields.StringField({ initial: "" }),
yeux: new fields.StringField({ initial: "" }),
description: new fields.HTMLField({ initial: "" }),
amemultiplier: new fields.NumberField({ initial: 2, integer: true }),
ignoreamemalus: new fields.BooleanField({ initial: false }),
ignoresantemalus: new fields.BooleanField({ initial: false }),
notes: new fields.HTMLField({ initial: "" }),
gmnotes: new fields.HTMLField({ initial: "" })
}),
// Template core
subactors: new fields.ArrayField(new fields.StringField(), { initial: [] }),
attributs: new fields.SchemaField({
adr: new fields.SchemaField({
label: new fields.StringField({ initial: "Adresse" }),
labelnorm: new fields.StringField({ initial: "adresse" }),
abbrev: new fields.StringField({ initial: "adr" }),
value: new fields.NumberField({ initial: 1, integer: true })
}),
pui: new fields.SchemaField({
label: new fields.StringField({ initial: "Puissance" }),
labelnorm: new fields.StringField({ initial: "puissance" }),
abbrev: new fields.StringField({ initial: "pui" }),
value: new fields.NumberField({ initial: 1, integer: true })
}),
cla: new fields.SchemaField({
label: new fields.StringField({ initial: "Clairvoyance" }),
labelnorm: new fields.StringField({ initial: "clairvoyance" }),
abbrev: new fields.StringField({ initial: "cla" }),
value: new fields.NumberField({ initial: 1, integer: true })
}),
pre: new fields.SchemaField({
label: new fields.StringField({ initial: "Présence" }),
labelnorm: new fields.StringField({ initial: "presence" }),
abbrev: new fields.StringField({ initial: "pre" }),
value: new fields.NumberField({ initial: 0, integer: true })
}),
tre: new fields.SchemaField({
label: new fields.StringField({ initial: "Trempe" }),
labelnorm: new fields.StringField({ initial: "trempe" }),
abbrev: new fields.StringField({ initial: "tre" }),
value: new fields.NumberField({ initial: 0, integer: true })
})
}),
bonneaventure: new fields.SchemaField({
base: new fields.NumberField({ initial: 0, integer: true }),
actuelle: new fields.NumberField({ initial: 0, integer: true })
}),
experience: new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true })
}),
eclat: new fields.SchemaField({
value: new fields.NumberField({ initial: 0, integer: true })
}),
sante: new fields.SchemaField({
base: new fields.NumberField({ initial: 0, integer: true }),
bonus: new fields.NumberField({ initial: 0, integer: true }),
nonletaux: new fields.NumberField({ initial: 0, integer: true }),
letaux: new fields.NumberField({ initial: 0, integer: true }),
malusmanuel: new fields.NumberField({ initial: 0, integer: true }),
sequelles: new fields.StringField({ initial: "" })
}),
ame: new fields.SchemaField({
fullmax: new fields.NumberField({ initial: 0, integer: true }),
currentmax: new fields.NumberField({ initial: 0, integer: true }),
value: new fields.NumberField({ initial: 0, integer: true }),
traumatismes: new fields.StringField({ initial: "" })
}),
invocationsElementaires: new fields.ArrayField(new fields.ObjectField(), { initial: [] }),
invocationsDemons: new fields.ArrayField(new fields.ObjectField(), { initial: [] }),
combat: new fields.SchemaField({
initbonus: new fields.NumberField({ initial: 0, integer: true }),
vitessebonus: new fields.NumberField({ initial: 0, integer: true }),
bonusdegats: new fields.NumberField({ initial: 0, integer: true }),
defensebonus: new fields.NumberField({ initial: 0, integer: true }),
monte: new fields.BooleanField({ initial: false })
}),
balance: new fields.SchemaField({
loi: new fields.NumberField({ initial: 0, integer: true }),
chaos: new fields.NumberField({ initial: 0, integer: true }),
aspect: new fields.NumberField({ initial: 0, integer: true }),
marge: new fields.NumberField({ initial: 0, integer: true }),
pointschaos: new fields.NumberField({ initial: 0, integer: true }),
pointsloi: new fields.NumberField({ initial: 0, integer: true })
})
};
}
}
+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: "" }),
};
}
}
+22
View File
@@ -0,0 +1,22 @@
/**
* Data model pour les protections
*/
export default class ProtectionDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" }),
typeprotection: new fields.StringField({ initial: "" }),
protection: new fields.NumberField({ initial: 0, integer: true }),
degats: new fields.StringField({ initial: "" }),
rarete: new fields.NumberField({ initial: 0, integer: true }),
prix: new fields.NumberField({ initial: 0, integer: true }),
equipped: new fields.BooleanField({ initial: false }),
enchantementLoi: new fields.SchemaField({
actif: new fields.BooleanField({ initial: false }),
bonus: new fields.NumberField({ initial: 0, integer: true }),
antiChaos: new fields.BooleanField({ initial: false }),
}),
};
}
}
+15
View File
@@ -0,0 +1,15 @@
/**
* Data model pour les runes
*/
export default class RuneDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" }),
formule: new fields.StringField({ initial: "" }),
seuil: new fields.NumberField({ initial: 0, integer: true }),
prononcee: new fields.StringField({ initial: "" }),
tracee: new fields.StringField({ initial: "" })
};
}
}
+15
View File
@@ -0,0 +1,15 @@
/**
* Data model pour les effets de runes
*/
export default class RuneEffectDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" }),
rune: new fields.StringField({ initial: "" }),
mode: new fields.StringField({ initial: "" }),
duree: new fields.StringField({ initial: "" }),
pointame: new fields.NumberField({ initial: 0, integer: true })
};
}
}
+12
View File
@@ -0,0 +1,12 @@
/**
* Data model pour les tendances
*/
export default class TendanceDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" }),
allegeance: new fields.StringField({ initial: "" })
};
}
}
+11
View File
@@ -0,0 +1,11 @@
/**
* Data model pour les traits chaotiques
*/
export default class TraitChaotiqueDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" })
};
}
}
+11
View File
@@ -0,0 +1,11 @@
/**
* Data model pour les traits d'espèce
*/
export default class TraitEspeceDataModel extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
description: new fields.HTMLField({ initial: "" })
};
}
}
+540 -98
View File
@@ -1,6 +1,10 @@
/* -------------------------------------------- */
import { MournbladeUtility } from "./mournblade-utility.js";
import { MournbladeRollDialog } from "./mournblade-roll-dialog.js";
import { MournbladeRollDialog } from "./applications/mournblade-roll-dialog.mjs";
import MournbladeInvocationDialog from "./applications/mournblade-invocation-dialog.mjs";
import MournbladeInvocationDemonDialog from "./applications/mournblade-invocation-demon-dialog.mjs";
import MournbladeInvocationEspritDialog from "./applications/mournblade-invocation-esprit-dialog.mjs";
import MournbladeEnchantementDialog from "./applications/mournblade-enchantement-dialog.mjs";
/* -------------------------------------------- */
const __degatsBonus = [-2, -2, -1, -1, 0, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 8, 8, 9, 9, 10, 10]
@@ -12,13 +16,41 @@ const __vitesseBonus = [-2, -2, -1, -1, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6
* @extends {Actor}
*/
export class MournbladeActor extends Actor {
prepareData() {
super.prepareData();
// Calculate derived attributes
const data = this.system;
if (this.type == 'personnage') {
// Compute base health and max soul from attributes (derived, no DB write needed)
data.sante.base = data.sante.bonus + (data.attributs.pui.value + data.attributs.tre.value) * 2 + 5;
data.ame.fullmax = (data.attributs.cla.value + data.attributs.tre.value) * data.biodata.amemultiplier + 5;
}
// Calculate total health
data.sante.total = data.sante.base + data.sante.bonus;
// Calculate current health
data.sante.current = data.sante.total - data.sante.nonletaux - data.sante.letaux;
// Calculate total ame
data.ame.total = data.ame.fullmax;
// Calculate balance
data.balance.total = data.balance.loi + data.balance.chaos;
}
prepareDerivedData() {
super.prepareDerivedData();
}
/* -------------------------------------------- */
/**
* Override the create() function to provide additional SoS functionality.
*
* This overrided create() function adds initial items
* Namely: Basic skills, money,
* This overrided create() function adds initial items
* Namely: Basic skills, money,
*
* @param {Object} data Barebones actor data which this function adds onto.
* @param {Object} options (Unused) Additional options which customize the creation workflow.
@@ -31,7 +63,7 @@ export class MournbladeActor extends Actor {
if (data instanceof Array) {
return super.create(data, options);
}
// If the created actor has items (only applicable to duplicated actors) bypass the new actor creation logic
// If the created actor has items (only applicable to foundry.utils.duplicated actors) bypass the new actor creation logic
if (data.items) {
let actor = super.create(data, options);
return actor;
@@ -41,47 +73,88 @@ export class MournbladeActor extends Actor {
const skills = await MournbladeUtility.loadCompendium("fvtt-mournblade.skills")
data.items = skills.map(i => i.toObject())
}
if (data.type == 'pnj') {
if (data.type == 'creature') {
const skills = await MournbladeUtility.loadCompendium("fvtt-mournblade.skills-creatures")
data.items = skills.map(i => i.toObject())
data.items.push({ name: "Arme naturelle 1", type: 'arme', img: "systems/fvtt-mournblade/assets/icons/arme.webp", system: { typearme: "contact", bonusmaniementoff: 0, seuildefense: 0, degats: "0" } })
data.items.push({ name: "Arme naturelle 2", type: 'arme', img: "systems/fvtt-mournblade/assets/icons/arme.webp", system: { typearme: "contact", bonusmaniementoff: 0, seuildefense: 0, degats: "0" } })
}
return super.create(data, options);
}
/* -------------------------------------------- */
setModifier(name, type, value) {
this.createEmbeddedDocuments("Item", [{ type: "modifier", name: name, system: { modifiertype: type, value: value } }])
ui.notifications.info("Le modificateur " + name + " a été ajouté à " + this.name + ".")
}
/* -------------------------------------------- */
prepareArme(arme) {
arme = duplicate(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.competence = duplicate(this.items.find(item => item.type == "competence" && item.name.toLowerCase() == "mêlée"))
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
if (arme.system.isdefense) {
arme.system.totalDefensif = combat.defenseTotal + arme.system.competence.system.niveau + arme.system.bonusmaniementdef
arme.system.isMelee = true
let competence = this.items.find(item => item.type == "competence" && item.name.toLowerCase() == "mêlée")
if (competence) {
arme.system.competence = foundry.utils.duplicate(competence)
arme.system.attrKey = "pui"
arme.system.totalDegats = arme.system.degats + "+" + combat.bonusDegatsTotal
arme.system.totalOffensif = this.system.attributs.pui.value + arme.system.competence.system.niveau + arme.system.bonusmaniementoff + combat.attaqueModifier + enchBonus
if (arme.system.isdefense) {
arme.system.totalDefensif = combat.defenseTotal + arme.system.competence.system.niveau + arme.system.bonusmaniementdef
}
} else {
arme.system.competence = null
arme.system.totalOffensif = 0
arme.system.totalDegats = arme.system.degats
arme.system.totalDefensif = 0
}
}
if (arme.system.typearme == "jet" || arme.system.typearme == "tir") {
arme.system.competence = duplicate(this.items.find(item => item.type == "competence" && item.name.toLowerCase() == "armes à distance"))
arme.system.attrKey = "adr"
arme.system.totalOffensif = this.system.attributs.adr.value + arme.system.competence.system.niveau + arme.system.bonusmaniementoff
arme.system.totalDegats = arme.system.degats
if (arme.system.isdefense) {
arme.system.totalDefensif = combat.defenseTotal + arme.system.competence.system.niveau + arme.system.bonusmaniementdef
arme.system.isDistance = true
let competence = this.items.find(item => item.type == "competence" && item.name.toLowerCase() == "armes à distance")
if (competence) {
arme.system.competence = foundry.utils.duplicate(competence)
arme.system.attrKey = "adr"
arme.system.totalOffensif = this.system.attributs.adr.value + arme.system.competence.system.niveau + arme.system.bonusmaniementoff + combat.attaqueModifier + enchBonus
arme.system.totalDegats = arme.system.degats
if (arme.system.isdefense) {
arme.system.totalDefensif = combat.defenseTotal + arme.system.competence.system.niveau + arme.system.bonusmaniementdef
}
} else {
arme.system.competence = null
arme.system.totalOffensif = 0
arme.system.totalDegats = arme.system.degats
arme.system.totalDefensif = 0
}
}
return arme
}
/* -------------------------------------------- */
prepareBouclier(bouclier) {
bouclier = duplicate(bouclier)
bouclier = foundry.utils.duplicate(bouclier)
let combat = this.getCombatValues()
bouclier.system.competence = duplicate(this.items.find(item => item.type == "competence" && item.name.toLowerCase() == "mêlée"))
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.isdefense = true
bouclier.system.bonusmaniementoff = 0
bouclier.system.totalDefensif = combat.defenseTotal + bouclier.system.competence.system.niveau + bouclier.system.bonusdefense
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 + enchBonus
bouclier.system.isdefense = true
bouclier.system.bonusmaniementoff = 0
bouclier.system.totalDefensif = combat.defenseTotal + bouclier.system.competence.system.niveau + bouclier.system.bonusdefense
} else {
bouclier.system.competence = null
bouclier.system.totalOffensif = 0
bouclier.system.totalDegats = bouclier.system.degats
bouclier.system.totalDefensif = 0
}
return bouclier
}
@@ -101,14 +174,31 @@ export class MournbladeActor extends Actor {
}
/* -------------------------------------------- */
getItemSorted( types) {
let items = this.items.filter(item => types.includes(item.type )) || []
getModifiersForRoll() {
let modifiers = []
for (let mod of this.items) {
if (mod.type == "modifier" && mod.system.modifiertype == "roll") {
let modObj = mod.toObject()
modObj.system.apply = true
modifiers.push(modObj)
}
}
MournbladeUtility.sortArrayObjectsByName(modifiers)
return modifiers
}
/* -------------------------------------------- */
getItemSorted(types) {
let items = this.items.filter(item => types.includes(item.type)) || []
MournbladeUtility.sortArrayObjectsByName(items)
return items
}
getDons() {
return this.getItemSorted(["don"])
}
getPactes() {
return this.getItemSorted(["pacte"])
}
getTendances() {
return this.getItemSorted(["tendance"])
}
@@ -118,15 +208,30 @@ export class MournbladeActor extends Actor {
getEquipments() {
return this.getItemSorted(["equipement"])
}
getModifiers() {
return this.getItemSorted(["modifier"])
}
getTraitsChaotiques() {
return this.getItemSorted(["traitchaotique"])
}
getTraitsEspeces() {
return this.getItemSorted(["traitespece"])
}
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"])
}
getOrigine() {
return this.items.find(item => item.type == "origine")
}
@@ -140,7 +245,7 @@ export class MournbladeActor extends Actor {
getSkills() {
let comp = []
for (let item of this.items) {
item = duplicate(item)
item = foundry.utils.duplicate(item)
if (item.type == "competence") {
item.system.attribut1total = item.system.niveau + (this.system.attributs[item.system.attribut1]?.value || 0)
item.system.attribut2total = item.system.niveau + (this.system.attributs[item.system.attribut2]?.value || 0)
@@ -160,12 +265,23 @@ export class MournbladeActor extends Actor {
return comp
}
/* -------------------------------------------- */
getProtectionTotal() {
let protection = 0
for (let item of this.items) {
if (item.type == "protection" && item.system.equipped) {
protection += item.system.protection
}
}
return protection
}
/* -------------------------------------------- */
getAspect() {
return (this.system.balance.loi > this.system.balance.chaos) ? this.system.balance.loi : this.system.balance.chaos
return (this.system.balance.loi > this.system.balance.chaos) ? this.system.balance.loi : this.system.balance.chaos
}
getMarge() {
return Math.abs( this.system.balance.loi - this.system.balance.chaos)
return Math.abs(this.system.balance.loi - this.system.balance.chaos)
}
getAlignement() {
return (this.system.balance.loi > this.system.balance.chaos) ? "loyal" : "chaotique"
@@ -178,12 +294,19 @@ 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))]
}
/* -------------------------------------------- */
getCombatValues() {
let defenserModifier = 0
this.items.filter(item => item.type == "modifier" && item.system.modifiertype == "defense").map(e => defenserModifier += e.system.value)
let attaqueModifier = 0
this.items.filter(item => item.type == "modifier" && item.system.modifiertype == "attaque").map(e => attaqueModifier += e.system.value)
let combat = {
defenserModifier,
attaqueModifier,
initBase: this.system.attributs.adr.value,
initTotal: this.system.attributs.adr.value + this.system.combat.initbonus,
bonusDegats: this.getBonusDegats(),
@@ -191,37 +314,11 @@ export class MournbladeActor extends Actor {
vitesseBase: this.getVitesseBase(),
vitesseTotal: this.getVitesseBase() + this.system.combat.vitessebonus,
defenseBase: this.getDefenseBase(),
defenseTotal: this.getDefenseBase() + this.system.combat.defensebonus
defenseTotal: this.getDefenseBase() + this.system.combat.defensebonus + defenserModifier
}
return combat
}
/* -------------------------------------------- */
prepareBaseData() {
}
/* -------------------------------------------- */
async prepareData() {
super.prepareData();
}
/* -------------------------------------------- */
prepareDerivedData() {
if (this.type == 'personnage') {
let newSante = this.system.sante.bonus + (this.system.attributs.pui.value + this.system.attributs.tre.value) * 2 + 5
if (this.system.sante.base != newSante) {
this.update({ 'system.sante.base': newSante })
}
let newAme = (this.system.attributs.cla.value + this.system.attributs.tre.value) * this.system.biodata.amemultiplier + 5
if (this.system.ame.fullmax != newAme) {
this.update({ 'system.ame.fullmax': newAme })
}
}
super.prepareDerivedData()
}
/* -------------------------------------------- */
_preUpdate(changed, options, user) {
@@ -232,7 +329,7 @@ export class MournbladeActor extends Actor {
getItemById(id) {
let item = this.items.find(item => item.id == id);
if (item) {
item = duplicate(item)
item = foundry.utils.duplicate(item)
}
return item;
}
@@ -240,12 +337,49 @@ export class MournbladeActor extends Actor {
/* -------------------------------------------- */
async equipItem(itemId) {
let item = this.items.find(item => item.id == itemId)
if (item && item.system) {
if (item?.system) {
let update = { _id: item.id, "system.equipped": !item.system.equipped }
await this.updateEmbeddedDocuments('Item', [update]); // Updates one EmbeddedEntity
}
}
/* -------------------------------------------- */
getStatusMalus() {
if (this.system.biodata.ignoresantemalus) {
return 0
}
let malusL = 0
let malusNL = 0
if (this.system.sante.base - this.system.sante.letaux < 10) {
malusL = -2
}
if (this.system.sante.base - this.system.sante.letaux < 5) {
malusL = -5
}
if (this.system.sante.base - this.system.sante.nonletaux < 10) {
malusNL = -2
}
if (this.system.sante.base - this.system.sante.nonletaux < 5) {
malusNL = -5
}
return Math.min(malusL, malusNL)
}
/* -------------------------------------------- */
getAmeMalus() {
if (this.system.biodata.ignoreamemalus) {
return 0
}
let malusA = 0
if (this.system.ame.currentmax - this.system.ame.value < 10) {
malusA = -2
}
if (this.system.ame.currentmax - this.system.ame.value < 5) {
malusA = -5
}
return malusA
}
/* -------------------------------------------- */
editItemField(itemId, itemType, itemField, dataType, value) {
let item = this.items.find(item => item.id == itemId)
@@ -261,16 +395,54 @@ export class MournbladeActor extends Actor {
}
}
/* -------------------------------------------- */
async incDecSante(type, value, applyArmure = true) {
value = Number(value)
if (value && applyArmure) {
let protection = this.getProtectionTotal()
value -= protection
value = Math.max(0, Number(value))
}
if (value) {
let newSante = foundry.utils.duplicate(this.system.sante)
newSante[type] += Number(value)
newSante[type] = Math.max(0, newSante[type])
if (newSante[type] > this.system.sante.base) {
value -= this.system.sante.base - newSante[type]
newSante[type] = this.system.sante.base
} else {
value = 0
}
newSante[type] = Math.min(newSante[type], newSante.base)
if (value && type == "nonletaux") {
newSante["letaux"] += value
}
await this.update({ 'system.sante': newSante })
}
}
/* -------------------------------------------- */
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)
await this.update({ 'system.ame': newAme })
}
}
/* -------------------------------------------- */
getBonneAventure() {
return this.system.bonneaventure.actuelle
}
/* -------------------------------------------- */
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 })
}
/* -------------------------------------------- */
@@ -279,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 })
}
/* -------------------------------------------- */
@@ -291,13 +463,13 @@ export class MournbladeActor extends Actor {
}
/* -------------------------------------------- */
subPointsAme(runeMode, value) {
let ame = duplicate(this.system.ame)
if(runeMode == "prononcer") {
ame.value -= value
let ame = foundry.utils.duplicate(this.system.ame)
if (runeMode == "prononcer") {
ame.value += value
} else {
ame.currentmax -= value
}
this.update( {'system.ame': ame})
return this.update({ 'system.ame': ame })
}
/* -------------------------------------------- */
@@ -313,12 +485,15 @@ export class MournbladeActor extends Actor {
/* -------------------------------------------- */
getAttribute(attrKey) {
return this.system.attributes[attrKey]
return this.system.attributs[attrKey]
}
/* -------------------------------------------- */
getBonusDegats() {
return __degatsBonus[this.system.attributs.pui.value]
if (this.type == "creature") {
return 0
}
return __degatsBonus[Math.min(21, Math.max(0, this.system.attributs.pui.value))]
}
/* -------------------------------------------- */
@@ -334,13 +509,13 @@ export class MournbladeActor extends Actor {
getSubActors() {
let subActors = [];
for (let id of this.system.subactors) {
subActors.push(duplicate(game.actors.get(id)));
subActors.push(foundry.utils.duplicate(game.actors.get(id)));
}
return subActors;
}
/* -------------------------------------------- */
async addSubActor(subActorId) {
let subActors = duplicate(this.system.subactors);
let subActors = foundry.utils.duplicate(this.system.subactors);
subActors.push(subActorId);
await this.update({ 'system.subactors': subActors });
}
@@ -371,34 +546,55 @@ export class MournbladeActor extends Actor {
/* -------------------------------------------- */
async setPredilectionUsed(compId, predIdx) {
let comp = this.items.get(compId)
let pred = duplicate(comp.system.predilections)
let pred = foundry.utils.duplicate(comp.system.predilections)
pred[predIdx].used = true
await this.updateEmbeddedDocuments('Item', [{ _id: compId, 'system.predilections': pred }])
}
/* -------------------------------------------- */
getInitiativeScore( ) {
getInitiativeScore() {
return Number(this.system.attributs.adr.value) + Number(this.system.combat.initbonus)
}
/* -------------------------------------------- */
getBestDefenseValue() {
let defenseList = this.items.filter(item => (item.type =="arme" || item.type == "bouclier") && item.system.equipped)
let defenseList = this.items.filter(item => (item.type == "arme" || item.type == "bouclier") && item.system.equipped)
let maxDef = 0
let bestArme
for(let arme of defenseList) {
for (let arme of defenseList) {
if (arme.type == "arme" && arme.system.isdefense) {
arme = this.prepareArme(arme)
}
if (arme.type == "bouclier" ) {
if (arme.type == "bouclier") {
arme = this.prepareBouclier(arme)
}
if ( arme.system.totalDefensif > maxDef) {
if (arme.system.totalDefensif > maxDef) {
maxDef = arme.system.totalDefensif
bestArme = duplicate(arme)
bestArme = foundry.utils.duplicate(arme)
}
}
return bestArme
}
/* -------------------------------------------- */
depenseRessources(arme) {
if (arme.system.nbressources && Number(arme.system.nbressources) > 0) {
if (this.type == "creature") {
let ressources = foundry.utils.duplicate(this.system.ressources)
if (Number(ressources.value) >= Number(arme.system.nbressources)) {
ressources.value -= arme.system.nbressources
this.update({ 'system.ressources': ressources })
ChatMessage.create({
content: "L'utilisation de la capacité/arme a dépensé " + arme.system.nbressources + " ressources.",
whisper: game.user.id,
user: game.user.id
});
} else {
ui.notifications.warn("Points de ressources insuffisants.")
}
} else {
ui.notifications.warn("Les ressources ne sont pas disponibles pour les personnages.")
}
}
}
/* -------------------------------------------- */
getCommonRollData(attrKey = undefined, compId = undefined, compName = undefined) {
@@ -411,20 +607,29 @@ export class MournbladeActor extends Actor {
rollData.canEclatDoubleD20 = this.canEclatDoubleD20()
rollData.doubleD20 = false
rollData.attributs = MournbladeUtility.getAttributs()
rollData.selectDifficulte = true
rollData.malusSante = this.getStatusMalus() + this.system.sante.malusmanuel
rollData.malusAme = this.getAmeMalus()
rollData.modifiers = this.getModifiersForRoll()
rollData.desavantages = {}
rollData.isMonte = this.system.combat.monte
if (rollData.isMonte) {
rollData.config.attaques["chargecavalerie"] = "Charge de cavalerie"
}
if (attrKey) {
rollData.attrKey = attrKey
if (attrKey != "tochoose") {
rollData.actionImg = "systems/fvtt-mournblade/assets/icons/" + this.system.attributs[attrKey].labelnorm + ".webp"
rollData.attr = duplicate(this.system.attributs[attrKey])
rollData.attr = foundry.utils.duplicate(this.system.attributs[attrKey])
}
}
if (compId) {
rollData.competence = duplicate(this.items.get(compId) || {})
rollData.competence = foundry.utils.duplicate(this.items.get(compId) || {})
rollData.actionImg = rollData.competence?.img
}
if (compName) {
rollData.competence = duplicate(this.items.find( item => item.name.toLowerCase() == compName.toLowerCase()) || {})
rollData.competence = foundry.utils.duplicate(this.items.find(item => item.name.toLowerCase() == compName.toLowerCase()) || {})
rollData.actionImg = rollData.competence?.img
}
return rollData
@@ -433,35 +638,201 @@ export class MournbladeActor extends Actor {
/* -------------------------------------------- */
async rollAttribut(attrKey) {
let rollData = this.getCommonRollData(attrKey)
let rollDialog = await MournbladeRollDialog.create(this, rollData)
rollDialog.render(true)
await MournbladeRollDialog.create(this, rollData)
}
/* -------------------------------------------- */
async rollCompetence(attrKey, compId) {
let rollData = this.getCommonRollData(attrKey, compId)
console.log("RollDatra", rollData)
let rollDialog = await MournbladeRollDialog.create(this, rollData)
rollDialog.render(true)
await MournbladeRollDialog.create(this, rollData)
}
/* -------------------------------------------- */
async rollRune(runeId) {
async rollRune(runeId) {
let comp = this.items.find(comp => comp.type == "competence" && comp.name.toLowerCase() == "savoir : runes")
if ( !comp) {
if (!comp) {
ui.notifications.warn("La compétence Savoirs : Runes n'a pas été trouvée, abandon.")
return
}
let rollData = this.getCommonRollData("cla", undefined, "Savoir : Runes")
rollData.rune = duplicate(this.items.get(runeId) || {})
rollData.rune = foundry.utils.duplicate(this.items.get(runeId) || {})
rollData.difficulte = rollData.rune?.system?.seuil || 0
rollData.runemode = "prononcer"
rollData.runeame = 1
rollData.runeame = 1
console.log("runeData", rollData)
let rollDialog = await MournbladeRollDialog.create(this, rollData)
rollDialog.render(true)
await MournbladeRollDialog.create(this, rollData)
}
/* -------------------------------------------- */
async rollSortilege() {
const runes = this.getRunes()
if (runes.length < 2) {
ui.notifications.warn("Il faut au moins deux Runes pour lancer un Sortilège.")
return
}
const comp = this.items.find(c => c.type == "competence" && c.name.toLowerCase() == "savoir : runes")
if (!comp) {
ui.notifications.warn("La compétence Savoir : Runes n'a pas été trouvée, abandon.")
return
}
let rollData = this.getCommonRollData("cla", undefined, "Savoir : Runes")
rollData.isSortilege = true
rollData.difficulte = 0
rollData.runemode = "prononcer"
rollData.runeame = 0
await MournbladeRollDialog.createSortilege(this, rollData)
}
/* -------------------------------------------- */
async preparePotion() {
const runes = this.getRunes()
if (runes.length === 0) {
ui.notifications.warn("Aucune Rune disponible pour préparer une potion.")
return
}
const comp = this.items.find(c => c.type == "competence" && c.name.toLowerCase() == "savoir : runes")
if (!comp) {
ui.notifications.warn("La compétence Savoir : Runes n'a pas été trouvée, abandon.")
return
}
let rollData = this.getCommonRollData("cla", comp._id)
rollData.isPotion = true
rollData.mainDice = "1d10"
// Optionally cap by Savoir:Haut-Parler and Savoir:Alchimie
const hautParlerComp = this.items.find(c => c.type == "competence" && c.name.toLowerCase() == "savoir : haut-parler")
const alchimieComp = this.items.find(c => c.type == "competence" && c.name.toLowerCase() == "savoir : alchimie")
if (hautParlerComp) rollData.limitHautParlerValue = hautParlerComp.system.niveau
if (alchimieComp) rollData.limitAlchimieValue = alchimieComp.system.niveau
await MournbladeRollDialog.createPotion(this, rollData)
}
/* -------------------------------------------- */
async invoquerElementaire() {
const persuasionComp = this.items.find(c => c.type == "competence" && c.name.toLowerCase() == "persuasion")
if (!persuasionComp) {
ui.notifications.warn("La compétence Persuasion est requise pour invoquer un Élémentaire.")
return
}
let rollData = this.getCommonRollData("pre", persuasionComp._id)
rollData.isInvocationElementaire = true
rollData.mainDice = "1d10"
const hautParlerComp = this.items.find(c => c.type == "competence" && c.name.toLowerCase() == "savoir : haut-parler")
const seigneursElemComp = this.items.find(c => c.type == "competence" && c.name.toLowerCase() == "savoir : seigneurs élémentaires")
if (hautParlerComp) rollData.hautParlerNiveau = hautParlerComp.system.niveau
if (seigneursElemComp) rollData.seigneursElemNiveau = seigneursElemComp.system.niveau
await MournbladeInvocationDialog.create(this, rollData)
}
/* -------------------------------------------- */
async bannirElementaire(invocIndex) {
const invocations = foundry.utils.duplicate(this.system.invocationsElementaires || [])
const invoc = invocations[invocIndex]
if (!invoc) return
// Free the blocked soul
await this.subPointsAme("prononcer", -invoc.soulCost)
// Delete the created actor if it still exists
if (invoc.actorId) {
const createdActor = game.actors.get(invoc.actorId)
if (createdActor) await createdActor.delete()
}
// Remove from the list
invocations.splice(invocIndex, 1)
await this.update({ "system.invocationsElementaires": invocations })
ui.notifications.info(`L'Élémentaire ${invoc.actorName} a été banni. ${invoc.soulCost} points d'Âme libérés.`)
}
/* -------------------------------------------- */
async invoquerDemon() {
const normalize = str => str.toLowerCase()
.replace(/œ/g, "oe").replace(/æ/g, "ae")
.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-z0-9]+/g, " ").trim()
const hasKeywords = (item, ...words) => {
const n = normalize(item.name)
return words.every(w => n.includes(normalize(w)))
}
const coercitionComp = this.items.find(c => c.type === "competence" && hasKeywords(c, "coercition"))
if (!coercitionComp) {
ui.notifications.warn("La compétence Coercition est requise pour invoquer un Démon.")
return
}
const isChaotique = this.system.balance.chaos > this.system.balance.loi
const hasOeilSorcier = this.items.some(c => (c.type === "capacite" || c.type === "don") && hasKeywords(c, "oeil", "sorcier"))
const hasRuneChaos = this.items.some(i => i.type === "rune" && hasKeywords(i, "chaos"))
if (!isChaotique || !hasOeilSorcier || !hasRuneChaos) {
const missing = []
if (!isChaotique) missing.push("alignement chaotique (Chaos > Loi)")
if (!hasOeilSorcier) missing.push("Capacité/Don Œil du Sorcier")
if (!hasRuneChaos) missing.push("Rune du Chaos")
ui.notifications.warn(`Prérequis manquants pour l'invocation démoniaque : ${missing.join(", ")}.`)
}
let rollData = this.getCommonRollData("tre", coercitionComp._id)
rollData.isInvocationDemon = true
rollData.mainDice = "1d10"
await MournbladeInvocationDemonDialog.create(this, rollData)
}
/* -------------------------------------------- */
async libererDemon(invocIndex) {
const invocations = foundry.utils.duplicate(this.system.invocationsDemons || [])
const invoc = invocations[invocIndex]
if (!invoc) return
invocations.splice(invocIndex, 1)
await this.update({ "system.invocationsDemons": invocations })
ui.notifications.info(`Le Démon ${invoc.demonName ?? "invoqué"} a été libéré.`)
}
/* -------------------------------------------- */
async invoquerEspritLoi() {
const normalize = str => str.toLowerCase()
.replace(/œ/g, "oe").replace(/æ/g, "ae")
.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-z0-9]+/g, " ").trim()
const hasKeywords = (item, ...words) => {
const n = normalize(item.name)
return words.every(w => n.includes(normalize(w)))
}
const persuasionComp = this.items.find(c => c.type === "competence" && hasKeywords(c, "persuasion"))
const isLoyal = this.system.balance.loi > this.system.balance.chaos
const hasRuneLoi = this.items.some(i => i.type === "rune" && hasKeywords(i, "loi"))
if (!isLoyal || !hasRuneLoi) {
const missing = []
if (!isLoyal) missing.push("alignement loyal (Loi > Chaos)")
if (!hasRuneLoi) missing.push("Rune de la Loi")
ui.notifications.warn(`Prérequis manquants : ${missing.join(", ")}.`)
}
const rollData = this.getCommonRollData("tre", persuasionComp?._id ?? null)
rollData.isInvocationEsprit = true
rollData.mainDice = "1d10"
await MournbladeInvocationEspritDialog.create(this, rollData)
}
/* -------------------------------------------- */
async enchanter(itemId) {
const item = this.items.get(itemId)
if (!item) return
if (!["arme", "equipement", "protection", "bouclier"].includes(item.type)) {
ui.notifications.warn("Seules les armes, équipements, protections et boucliers peuvent être enchantés.")
return
}
await MournbladeEnchantementDialog.create(this, item)
}
/* -------------------------------------------- */
async rollArmeOffensif(armeId) {
let arme = this.items.get(armeId)
@@ -473,9 +844,71 @@ export class MournbladeActor extends Actor {
}
let rollData = this.getCommonRollData(arme.system.attrKey, arme.system.competence._id)
rollData.arme = arme
rollData.typeAttaque = "assaut"
rollData.typeCouvert = "aucun"
rollData.hasDesavantageBonus = true
rollData.visee = false
rollData.ciblecourt = false
rollData.cibleconsciente = false
// Do not display difficulte if defense weapon or distance
if (rollData.armeDefense || rollData.arme.system.isDistance) {
rollData.selectDifficulte = false
rollData.difficulte = (rollData.arme.system.isDistance) ? 0 : rollData.difficulte
}
console.log("ARME!", rollData)
let rollDialog = await MournbladeRollDialog.create(this, rollData)
rollDialog.render(true)
this.depenseRessources(arme)
await MournbladeRollDialog.create(this, rollData)
}
/* -------------------------------------------- */
async rollAssomer() {
let rollData = this.getCommonRollData("adr", undefined, "Filouterie")
rollData.typeAttaque = "assomer"
rollData.typeCouvert = "aucun"
rollData.hasDesavantageBonus = true
if (rollData.defender) {
rollData.selectDifficulte = false
rollData.difficulte = rollData.defender.system.attributs.tre.value * 2
}
console.log("Assomer!", rollData)
await MournbladeRollDialog.create(this, rollData)
}
/* -------------------------------------------- */
async rollFuir() {
let rollData = this.getCommonRollData("adr", undefined, "Mouvements")
rollData.typeAttaque = "fuir"
rollData.typeCouvert = "aucun"
rollData.hasDesavantageBonus = true
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)
}
console.log("Fuir!", rollData)
await MournbladeRollDialog.create(this, rollData)
}
/* -------------------------------------------- */
async rollImmobiliser() {
let rollData = this.getCommonRollData("pui", undefined, "Mêlée")
rollData.typeAttaque = "immobiliser"
rollData.typeCouvert = "aucun"
rollData.hasDesavantageBonus = true
if (rollData.defender) {
rollData.selectDifficulte = false
rollData.difficulte = rollData.defenderCombatValues.defenseTotal
}
console.log("Immobiliser!", rollData)
await MournbladeRollDialog.create(this, rollData)
}
/* -------------------------------------------- */
async rollArmeSpecial(armeId) {
let arme = this.items.get(armeId)
if (arme) {
MournbladeUtility.createChatWithRollMode("GM", {
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-mournblade/templates/chat-display-description.hbs`, arme)
}, arme)
this.depenseRessources(arme)
}
}
/* -------------------------------------------- */
@@ -487,9 +920,18 @@ export class MournbladeActor extends Actor {
if (arme.type == "bouclier") {
arme = this.prepareBouclier(arme)
}
let roll = new Roll(arme.system.totalDegats).roll({ async: false })
//Unused rollData.degatsFormula = arme.system.totalDegats
let roll
try {
roll = await new Roll(arme.system.totalDegats).evaluate()
} catch (err) {
ui.notifications.error(`Formule de dégâts invalide : ${arme.system.totalDegats}`)
console.error("rollArmeDegats error:", err)
return
}
await MournbladeUtility.showDiceSoNice(roll, game.settings.get("core", "rollMode"));
let rollData = {
degatsFormula: arme.system.totalDegats,
arme: arme,
finalResult: roll.total,
alias: this.name,
@@ -498,7 +940,7 @@ export class MournbladeActor extends Actor {
actionImg: arme.img,
}
MournbladeUtility.createChatWithRollMode(rollData.alias, {
content: await renderTemplate(`systems/fvtt-mournblade/templates/chat-degats-result.html`, rollData)
content: await foundry.applications.handlebars.renderTemplate(`systems/fvtt-mournblade/templates/chat-degats-result-v2.hbs`, rollData)
})
}
+1 -1
View File
@@ -10,7 +10,7 @@ export class MournbladeCombat extends Combat {
const c = this.combatants.get(ids[cId]);
let id = c._id || c.id;
let initBonus = c.actor ? c.actor.getInitiativeScore() : 0
let roll = new Roll("1d10 + "+initBonus).roll({ async: false})
let roll = await new Roll("1d10 + "+initBonus).evaluate()
await MournbladeUtility.showDiceSoNice(roll, game.settings.get("core", "rollMode"))
//console.log("Init bonus", initBonus, roll.total)
await this.updateEmbeddedDocuments("Combatant", [ { _id: id, initiative: roll.total } ]);
+4 -5
View File
@@ -89,7 +89,7 @@ export class MournbladeCommands {
if (command && command.func) {
const result = command.func(content, msg, params);
if (result == false) {
RdDCommands._chatAnswer(msg, command.descr);
MournbladeCommands._chatAnswer(msg, command.descr);
}
return true;
}
@@ -98,8 +98,8 @@ export class MournbladeCommands {
/* -------------------------------------------- */
async createChar(msg) {
game.system.Mournblade.creator = new MournbladeActorCreate();
game.system.Mournblade.creator.start();
game.system.mournblade.creator = new MournbladeActorCreate();
game.system.mournblade.creator.start();
}
/* -------------------------------------------- */
@@ -116,8 +116,7 @@ export class MournbladeCommands {
rollData.mode = "generic"
rollData.title = `Dice Pool Roll`;
let rollDialog = await MournbladeRollDialog.create( this, rollData);
rollDialog.render( true );
await MournbladeRollDialog.create( this, rollData);
}
}
+149
View File
@@ -0,0 +1,149 @@
export class MournbladeConfig {
static getConfig() {
let MOURNBLADE_CONFIG = {
attaques: {
assaut: game.i18n.localize("MNBL.assaut"),
precise: game.i18n.localize("MNBL.preciseattack"),
feinte: game.i18n.localize("MNBL.feint"),
coupbas: game.i18n.localize("MNBL.dirtyattack"),
charger: game.i18n.localize("MNBL.charge"),
contenir: game.i18n.localize("MNBL.contain"),
desarmer: game.i18n.localize("MNBL.disarm")
},
couverts: {
aucun: { name: game.i18n.localize("MNBL.none"), value: 0 },
rondache: { name: game.i18n.localize("MNBL.lightcover"), value: -2 },
pavois: { name: game.i18n.localize("MNBL.mediumcover"), value: -5 },
complet: { name: game.i18n.localize("MNBL.heavycover"), value: -10 },
},
modifierTypes: {
aucun: { name: game.i18n.localize("MNBL.none"), value: 0 },
roll: { name: game.i18n.localize("MNBL.roll"), value: 0 },
degats: { name: game.i18n.localize("MNBL.damage"), value: 0 },
defense: { name: game.i18n.localize("MNBL.defensecapacity"), value: 0 },
attaque: { name: game.i18n.localize("MNBL.attackcapacity"), value: 0 },
},
listeNiveau: {
},
listeNiveauCreature: {
},
listePortees: {
"10": game.i18n.localize("MNBL.lessthanshort"),
"15": game.i18n.localize("MNBL.shortmore"),
"20": game.i18n.localize("MNBL.mediummore"),
"25": game.i18n.localize("MNBL.longmore")
},
modificateurOptions: {},
pointsAmeOptions: {},
difficulteOptions: {
"0": game.i18n.localize("MNBL.noneunknwon"),
"5": game.i18n.localize("MNBL.easy"),
"10": game.i18n.localize("MNBL.medium"),
"15": game.i18n.localize("MNBL.hard"),
"20": game.i18n.localize("MNBL.hazardous"),
"25": game.i18n.localize("MNBL.insane"),
"30": game.i18n.localize("MNBL.puremadness")
},
attributs: {
adr: game.i18n.localize("Adresse"), pui: game.i18n.localize("Puissance"),
cla: game.i18n.localize("Clairvoyance"), pre: game.i18n.localize("Présence"), tre: game.i18n.localize("Trempe")
},
lancementRuneOptions: {
prononcer: game.i18n.localize("MNBL.pronouncerune"),
inscrire: game.i18n.localize("MNBL.tracerune")
},
effetRuneOptions: {
prononcee: game.i18n.localize("MNBL.pronounced"),
inscrite: game.i18n.localize("MNBL.traced")
},
typeArmeOptions: {
contact: game.i18n.localize("MNBL.meleeweapon"),
contactjet: game.i18n.localize("MNBL.meleethrowweapon"),
jet: game.i18n.localize("MNBL.throwweapon"),
tir: game.i18n.localize("MNBL.shootweapon"),
special: game.i18n.localize("MNBL.specialweapon")
},
allegeanceOptions: {
tous: game.i18n.localize("MNBL.all"),
chaos: game.i18n.localize("MNBL.chaos"),
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"),
}
}
return MOURNBLADE_CONFIG;
}
};
+21 -20
View File
@@ -4,14 +4,14 @@ import { MournbladeUtility } from "./mournblade-utility.js";
* Extend the basic ItemSheet with some very simple modifications
* @extends {ItemSheet}
*/
export class MournbladeItemSheet extends ItemSheet {
export class MournbladeItemSheet extends foundry.appv1.sheets.ItemSheet {
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["fvtt-mournblade", "sheet", "item"],
template: "systems/fvtt-mournblade/templates/item-sheet.html",
template: "systems/fvtt-mournblade/templates/item-sheet.hbs",
dragDrop: [{ dragSelector: null, dropSelector: null }],
width: 620,
height: 550
@@ -48,8 +48,8 @@ export class MournbladeItemSheet extends ItemSheet {
/* -------------------------------------------- */
async getData() {
const objectData = duplicate(this.object)
let itemData = objectData
const objectData = foundry.utils.duplicate(this.object)
let itemData = objectData
let formData = {
title: this.title,
id: this.id,
@@ -63,13 +63,14 @@ export class MournbladeItemSheet extends ItemSheet {
limited: this.object.limited,
options: this.options,
owner: this.document.isOwner,
description: await TextEditor.enrichHTML(this.object.system.description, {async: true}),
description: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.object.system.description, {async: true}),
config: game.system.mournblade.config,
mr: (this.object.type == 'specialisation'),
isGM: game.user.isGM
}
if ( objectData.type == "don") {
formData.sacrifice = await TextEditor.enrichHTML(this.object.system.sacrifice, {async: true})
formData.sacrifice = await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.object.system.sacrifice, {async: true})
}
//this.options.editable = !(this.object.origin == "embeddedItem");
console.log("ITEM DATA", formData, this);
@@ -90,7 +91,7 @@ export class MournbladeItemSheet extends ItemSheet {
/* -------------------------------------------- */
postItem() {
let chatData = duplicate(MournbladeUtility.data(this.item));
let chatData = foundry.utils.duplicate(MournbladeUtility.data(this.item));
if (this.actor) {
chatData.actor = { id: this.actor.id };
}
@@ -105,7 +106,7 @@ export class MournbladeItemSheet extends ItemSheet {
payload: chatData,
});
renderTemplate('systems/fvtt-Mournblade-rpg/templates/post-item.html', chatData).then(html => {
foundry.applications.handlebars.renderTemplate('systems/fvtt-mournblade/templates/post-item.hbs', chatData).then(html => {
let chatOptions = MournbladeUtility.chatDataSetup(html);
ChatMessage.create(chatOptions)
});
@@ -133,28 +134,28 @@ export class MournbladeItemSheet extends ItemSheet {
html.find('.edit-prediction').change(ev => {
const li = $(ev.currentTarget).parents(".prediction-item")
let index = li.data("prediction-index")
let pred = duplicate(this.object.system.predilections)
let pred = foundry.utils.duplicate(this.object.system.predilections)
pred[index].name = ev.currentTarget.value
this.object.update( { 'data.predilections': pred })
this.object.update( { 'system.predilections': pred })
})
html.find('.delete-prediction').click(ev => {
const li = $(ev.currentTarget).parents(".prediction-item")
let index = li.data("prediction-index")
let pred = duplicate(this.object.system.predilections)
let pred = foundry.utils.duplicate(this.object.system.predilections)
pred.splice(index,1)
this.object.update( { 'data.predilections': pred })
this.object.update( { 'system.predilections': pred })
})
html.find('.use-prediction').change(ev => {
const li = $(ev.currentTarget).parents(".prediction-item")
let index = li.data("prediction-index")
let pred = duplicate(this.object.system.predilections)
let pred = foundry.utils.duplicate(this.object.system.predilections)
pred[index].used = ev.currentTarget.checked
this.object.update( { 'data.predilections': pred })
})
this.object.update( { 'system.predilections': pred })
})
html.find('#add-predilection').click(ev => {
let pred = duplicate(this.object.system.predilections)
pred.push( { name: "Nouvelle prédilection", used: false })
this.object.update( { 'data.predilections': pred })
let pred = foundry.utils.duplicate(this.object.system.predilections)
pred.push( { name: "Nouvelle prédilection", id: randomID(), used: false })
this.object.update( { 'system.predilections': pred })
})
// Update Inventory Item
html.find('.item-delete').click(ev => {
@@ -168,7 +169,7 @@ export class MournbladeItemSheet extends ItemSheet {
/* -------------------------------------------- */
get template() {
let type = this.item.type;
return `systems/fvtt-mournblade/templates/item-${type}-sheet.html`;
return `systems/fvtt-mournblade/templates/item-${type}-sheet.hbs`;
}
/* -------------------------------------------- */
+15 -12
View File
@@ -1,18 +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",
tendance: "systems/fvtt-mournblade/assets/icons/tendance.webp",
traitchaotique: "systems/fvtt-mournblade/assets/icons/traitchaotique.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",
}
/**
+105 -51
View File
@@ -10,11 +10,19 @@
// Import Modules
import { MournbladeActor } from "./mournblade-actor.js";
import { MournbladeItemSheet } from "./mournblade-item-sheet.js";
import { MournbladeActorSheet } from "./mournblade-actor-sheet.js";
//import { MournbladeNPCSheet } from "./mournblade-npc-sheet.js";
// import { MournbladeActorSheet } from "./mournblade-actor-sheet.js";
// import { MournbladeCreatureSheet } from "./mournblade-creature-sheet.js";
import { MournbladeUtility } from "./mournblade-utility.js";
import { MournbladeCombat } from "./mournblade-combat.js";
import { MournbladeItem } from "./mournblade-item.js";
import { MournbladeConfig } from "./mournblade-config.js";
// Import DataModels
import * as models from "./models/index.mjs";
// Import AppV2 Item Sheets
import * as sheets from "./applications/sheets/_module.mjs";
import { MournbladeRollDialog } from "./mournblade-roll-dialog.js";
/* -------------------------------------------- */
/* Foundry VTT Initialization */
@@ -29,14 +37,14 @@ Hooks.once("init", async function () {
MournbladeUtility.preloadHandlebarsTemplates();
/* -------------------------------------------- */
// Set an initiative formula for the system
// Set an initiative formula for the system
CONFIG.Combat.initiative = {
formula: "1d6",
decimals: 1
};
/* -------------------------------------------- */
game.socket.on("system.fvtt-mournblade-rpg", data => {
game.socket.on("system.fvtt-mournblade", data => {
MournbladeUtility.onSocketMesssage(data);
});
@@ -44,82 +52,129 @@ Hooks.once("init", async function () {
// Define custom Entity classes
CONFIG.Combat.documentClass = MournbladeCombat
CONFIG.Actor.documentClass = MournbladeActor
CONFIG.Actor.dataModels = {
personnage: models.PersonnageDataModel,
creature: models.CreatureDataModel,
}
CONFIG.Item.documentClass = MournbladeItem
game.system.mournblade = { }
CONFIG.Item.dataModels = {
arme: models.ArmeDataModel,
bouclier: models.BouclierDataModel,
capacite: models.CapaciteDataModel,
competence: models.CompetenceDataModel,
don: models.DonDataModel,
equipement: models.EquipementDataModel,
heritage: models.HeritageDataModel,
metier: models.MetierDataModel,
modifier: models.ModifierDataModel,
monnaie: models.MonnaieDataModel,
origine: models.OrigineDataModel,
pacte: models.PacteDataModel,
protection: models.ProtectionDataModel,
rune: models.RuneDataModel,
runeeffect: models.RuneEffectDataModel,
potion: models.PotionDataModel,
tendance: models.TendanceDataModel,
traitchaotique: models.TraitChaotiqueDataModel,
traitespece: models.TraitEspeceDataModel
}
game.system.mournblade = {
config : MournbladeConfig.getConfig(),
models,
sheets,
MournbladeRollDialog
}
/* -------------------------------------------- */
// Register sheet application classes
Actors.unregisterSheet("core", ActorSheet);
Actors.registerSheet("fvtt-mournblade", MournbladeActorSheet, { types: ["personnage"], makeDefault: true })
//Actors.registerSheet("fvtt-mournblade", MournbladeNPCSheet, { types: ["npc"], makeDefault: false });
foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet);
foundry.documents.collections.Actors.registerSheet("fvtt-mournblade", sheets.MournbladePersonnageSheet, { types: ["personnage"], makeDefault: true });
foundry.documents.collections.Actors.registerSheet("fvtt-mournblade", sheets.MournbladeCreatureSheet, { types: ["creature"], makeDefault: true });
Items.unregisterSheet("core", ItemSheet);
Items.registerSheet("fvtt-mournblade", MournbladeItemSheet, { makeDefault: true })
foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ItemSheet);
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeArmeSheet, { types: ["arme"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeBouclierSheet, { types: ["bouclier"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeCapaciteSheet, { types: ["capacite"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeCompetenceSheet, { types: ["competence"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeDonSheet, { types: ["don"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeEquipementSheet, { types: ["equipement"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeHeritageSheet, { types: ["heritage"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeMetierSheet, { types: ["metier"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeModifierSheet, { types: ["modifier"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeMonnaieSheet, { types: ["monnaie"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeOrigineSheet, { types: ["origine"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladePacteSheet, { types: ["pacte"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeProtectionSheet, { types: ["protection"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeRuneSheet, { types: ["rune"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeRuneEffectSheet, { types: ["runeeffect"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladePotionSheet, { types: ["potion"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeTendanceSheet, { types: ["tendance"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeTraitChaotiqueSheet, { types: ["traitchaotique"], makeDefault: true });
foundry.documents.collections.Items.registerSheet("fvtt-mournblade", sheets.MournbladeTraitEspeceSheet, { types: ["traitespece"], makeDefault: true });
MournbladeUtility.init();
});
/* -------------------------------------------- */
function welcomeMessage() {
ChatMessage.create({
async function welcomeMessage() {
const templateData = {};
const html = await foundry.applications.handlebars.renderTemplate("systems/fvtt-mournblade/templates/chat-welcome-message.hbs", templateData);
await ChatMessage.create({
user: game.user.id,
whisper: [game.user.id],
content: `<div id="welcome-message-Mournblade"><span class="rdd-roll-part">
<strong>Bienvenue dans les Jeunes Royaumes de Mournblade !</strong>
<p>Les livres de Mournblade sont nécessaires pour jouer : https://www.titam-france.fr</p>
<p>Mournblade est jeu de rôle publié par Titam France/Sombres projets, tout les droits leur appartiennent.</p>
<p>Système développé par LeRatierBretonnien, support sur le <a href="https://discord.gg/pPSDNJk">Discord FR de Foundry</a>.</p>
` });
content: html
});
}
/* -------------------------------------------- */
// Register world usage statistics
function registerUsageCount( registerKey ) {
if ( game.user.isGM ) {
game.settings.register(registerKey, "world-key", {
name: "Unique world key",
scope: "world",
config: false,
default: "",
type: String
});
let worldKey = game.settings.get(registerKey, "world-key")
if ( worldKey == undefined || worldKey == "" ) {
worldKey = randomID(32)
game.settings.set(registerKey, "world-key", worldKey )
}
// Simple API counter
let regURL = `https://www.uberwald.me/fvtt_appcount/count.php?name="${registerKey}"&worldKey="${worldKey}"&version="${game.release.generation}.${game.release.build}"&system="${game.system.id}"&systemversion="${game.system.version}"`
//$.ajaxSetup({
//headers: { 'Access-Control-Allow-Origin': '*' }
//})
$.ajax(regURL)
async function importDefaultScene() {
let exists = game.scenes.find(j => j.name == "Accueil");
if (!exists) {
const scenes = await MournbladeUtility.loadCompendium("fvtt-mournblade.scenes")
let newDocuments = scenes.filter(i => i.name == "Accueil");
await game.scenes.documentClass.create(newDocuments);
game.scenes.find(i => i.name == "Accueil").activate();
}
}
/* -------------------------------------------- */
/* Foundry VTT Initialization */
/* -------------------------------------------- */
Hooks.once("ready", function () {
Hooks.once("ready", async function () {
game.system.mournblade = {
config : MournbladeConfig.getConfig(),
}
MournbladeUtility.ready();
// User warning
if (!game.user.isGM && game.user.character == undefined) {
ui.notifications.info("Attention ! Aucun personnage n'est relié au joueur !");
ChatMessage.create({
await ChatMessage.create({
content: "<b>ATTENTION</b> Le joueur " + game.user.name + " n'est relié à aucun personnage !",
user: game.user._id
user: game.user.id
});
}
// CSS patch for v9
if (game.version) {
let sidebar = document.getElementById("sidebar");
sidebar.style.width = "min-content";
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 !");
await ChatMessage.create({
content: "<b>ATTENTION</b> Le token du joueur " + game.user.name + " n'est pas connecté à l'acteur !",
user: game.user.id
});
}
registerUsageCount('fvtt-mournblade')
import("https://www.uberwald.me/fvtt_appcount/count-class-ready.js").then(moduleCounter=>{
console.log("ClassCounter loaded", moduleCounter)
moduleCounter.ClassCounter.registerUsageCount()
}).catch(err=>
console.log("No stats available, giving up.")
)
importDefaultScene();
welcomeMessage();
});
@@ -136,4 +191,3 @@ Hooks.on("chatMessage", (html, content, msg) => {
}
return true;
});
+82 -41
View File
@@ -1,79 +1,120 @@
import { MournbladeUtility } from "./mournblade-utility.js";
export class MournbladeRollDialog extends Dialog {
export class MournbladeRollDialog extends Application {
/* -------------------------------------------- */
static async create(actor, rollData ) {
let options = { classes: ["MournbladeDialog"], width: 340, height: 'fit-content', 'z-index': 99999 };
let html = await renderTemplate('systems/fvtt-mournblade/templates/roll-dialog-generic.html', rollData);
return new MournbladeRollDialog(actor, rollData, html, options );
return new MournbladeRollDialog(actor, rollData);
}
/* -------------------------------------------- */
constructor(actor, rollData, html, options, close = undefined) {
let conf = {
title: "Test de Capacité",
content: html,
buttons: {
rolld10: {
icon: '<i class="fas fa-check"></i>',
label: "Lancer 1d10",
callback: () => { this.roll("1d10") }
},
rolld20: {
icon: '<i class="fas fa-check"></i>',
label: "Lancer 1d20",
callback: () => { this.roll("1d20") }
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: "Annuler",
callback: () => { this.close() }
} },
close: close
constructor(actor, rollData, options = {}) {
super(options);
this.actor = actor;
this.rollData = rollData;
}
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["fvtt-mournblade", "sheet", "item"],
template: "systems/fvtt-mournblade/templates/roll-dialog-generic.hbs",
width: 400,
height: "auto",
title: "Test de Capacité"
});
}
getData() {
const data = foundry.utils.duplicate(this.rollData);
if (!data.config) {
data.config = game.system.mournblade.config;
}
return data;
}
super(conf, options);
this.actor = actor
this.rollData = rollData
_onCancel() {
this.close();
}
/* -------------------------------------------- */
roll ( dice) {
this.rollData.mainDice = dice
MournbladeUtility.rollMournblade( this.rollData )
_onRoll(dice) {
this.rollData.mainDice = dice;
MournbladeUtility.rollMournblade(this.rollData);
this.close();
}
/* -------------------------------------------- */
activateListeners(html) {
super.activateListeners(html);
// Roll buttons
html.find(".rolld10").click(this._onRoll.bind(this, "1d10"));
html.find(".rolld20").click(this._onRoll.bind(this, "1d20"));
html.find(".cancel").click(this._onCancel.bind(this));
var dialog = this;
function onLoad() {
}
$(function () { onLoad(); });
html.find('.apply-modifier').change(async (event) => {
let modifierIdx = $(event.currentTarget).data("modifier-idx")
let modifier = this.rollData.modifiers[modifierIdx]
modifier.system.apply = event.currentTarget.checked
})
html.find('#modificateur').change(async (event) => {
this.rollData.modificateur = Number(event.currentTarget.value)
})
html.find('#typeAttaque').change(async (event) => {
this.rollData.typeAttaque = String(event.currentTarget.value)
})
html.find('#difficulte').change(async (event) => {
this.rollData.difficulte = Number(event.currentTarget.value)
})
html.find('#attrKey').change(async (event) => {
this.rollData.attrKey = String(event.currentTarget.value)
})
})
html.find('#runemode').change(async (event) => {
this.rollData.runemode = String(event.currentTarget.value)
})
})
html.find('#runeame').change(async (event) => {
this.rollData.runeame = Number(event.currentTarget.value)
})
})
html.find('#isMonte').change(async (event) => {
this.rollData.desavantages.isMonte = event.currentTarget.checked
})
html.find('#cibleausol').change(async (event) => {
this.rollData.desavantages.cibleausol = event.currentTarget.checked
})
html.find('#cibledesarmee').change(async (event) => {
this.rollData.desavantages.cibledesarmee = event.currentTarget.checked
})
html.find('#ciblerestreint').change(async (event) => {
this.rollData.desavantages.ciblerestreint = event.currentTarget.checked
})
html.find('#cibleimmobilisée').change(async (event) => {
this.rollData.desavantages.cibleimmobilisée = event.currentTarget.checked
})
html.find('#ciblesurplomb').change(async (event) => {
this.rollData.desavantages.ciblesurplomb = event.currentTarget.checked
})
html.find('#doubleD20').change(async (event) => {
this.rollData.doubleD20 = event.currentTarget.checked
})
})
html.find('#visee').change(async (event) => {
this.rollData.visee = event.currentTarget.checked
})
html.find('#cibleconsciente').change(async (event) => {
this.rollData.cibleconsciente = event.currentTarget.checked
})
html.find('#ciblecourt').change(async (event) => {
this.rollData.ciblecourt = event.currentTarget.checked
})
html.find('#typeCouvert').change(async (event) => {
this.rollData.typeCouvert = String(event.currentTarget.value)
})
}
}
File diff suppressed because it is too large Load Diff
+5707
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
{
"name": "fvtt-mournblade",
"version": "1.0.0",
"description": "Mournblade RPG for FoundryVTT - French",
"scripts": {
"build": "gulp build",
"watch": "gulp watch"
},
"author": "Uberwald/LeRatierBretonnien",
"license": "SEE LICENSE IN LICENCE.txt",
"devDependencies": {
"@foundryvtt/foundryvtt-cli": "^3.0.3",
"gulp": "^4.0.2",
"gulp-less": "^5.0.0",
"gulp-sourcemaps": "^3.0.0"
}
}
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000010
MANIFEST-000404
+7 -8
View File
@@ -1,8 +1,7 @@
2023/12/19-21:45:05.580403 7f3323fff6c0 Recovering log #8
2023/12/19-21:45:05.635099 7f3323fff6c0 Delete type=3 #6
2023/12/19-21:45:05.635255 7f3323fff6c0 Delete type=0 #8
2023/12/19-21:45:32.368329 7f33223ff6c0 Level-0 table #13: started
2023/12/19-21:45:32.368364 7f33223ff6c0 Level-0 table #13: 0 bytes OK
2023/12/19-21:45:32.374930 7f33223ff6c0 Delete type=0 #11
2023/12/19-21:45:32.386871 7f33223ff6c0 Manual compaction at level-0 from '!items!0swiE8k5zfUIqmXu' @ 72057594037927935 : 1 .. '!items!wv5EiePmPTpqFutt' @ 0 : 0; will stop at (end)
2023/12/19-21:45:32.393727 7f33223ff6c0 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 @@
2023/12/19-21:42:28.620956 7f3322ffd6c0 Recovering log #4
2023/12/19-21:42:28.631643 7f3322ffd6c0 Delete type=3 #2
2023/12/19-21:42:28.631756 7f3322ffd6c0 Delete type=0 #4
2023/12/19-21:44:45.435469 7f33223ff6c0 Level-0 table #9: started
2023/12/19-21:44:45.435531 7f33223ff6c0 Level-0 table #9: 0 bytes OK
2023/12/19-21:44:45.477657 7f33223ff6c0 Delete type=0 #7
2023/12/19-21:44:45.478091 7f33223ff6c0 Manual compaction at level-0 from '!items!0swiE8k5zfUIqmXu' @ 72057594037927935 : 1 .. '!items!wv5EiePmPTpqFutt' @ 0 : 0; will stop at (end)
2023/12/19-21:44:45.478174 7f33223ff6c0 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.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
MANIFEST-000023

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