Compare commits

..

5 Commits

Author SHA1 Message Date
uberwald caef2c85d2 Fix release numbering 2026-06-14 23:25:41 +02:00
uberwald e43487b727 Fix release numbering 2026-06-14 23:25:24 +02:00
uberwald 709e69fac8 Fix progression roll
Release Creation / build (release) Successful in 1m33s
2026-04-29 09:34:16 +02:00
uberwald e26db56585 Fix for v14
Release Creation / build (release) Successful in 48s
2026-04-28 07:52:18 +02:00
uberwald bb6a6248f2 Foundryv14 migration
Release Creation / build (release) Successful in 46s
2026-04-01 22:57:43 +02:00
60 changed files with 623 additions and 232 deletions
+1
View File
@@ -0,0 +1 @@
packs/** filter=lfs diff=lfs merge=lfs -text
+2 -2
View File
@@ -59,5 +59,5 @@ jobs:
version: ${{github.event.release.tag_name}} version: ${{github.event.release.tag_name}}
manifest: 'https://www.uberwald.me/gitea/public/fvtt-cthulhu-eternal/releases/download/latest/system.json' manifest: 'https://www.uberwald.me/gitea/public/fvtt-cthulhu-eternal/releases/download/latest/system.json'
notes: 'https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{github.event.release.tag_name}}/fvtt-cthulhu-eternal.zip' notes: 'https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{github.event.release.tag_name}}/fvtt-cthulhu-eternal.zip'
compatibility-minimum: '13' compatibility-minimum: '14'
compatibility-verified: '13' compatibility-verified: '14'
+2
View File
@@ -5,4 +5,6 @@ styles/*.css
# Node Modules # Node Modules
node_modules/ node_modules/
.github/
.history/
+5
View File
@@ -0,0 +1,5 @@
{
"cSpell.words": [
"CTHULHUETERNAL"
]
}
+45 -3
View File
@@ -677,6 +677,12 @@ i.fvtt-cthulhu-eternal {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 4px; gap: 4px;
} }
.fvtt-cthulhu-eternal .empty-state {
color: var(--color-text-light-6);
font-style: italic;
padding: 0.5rem 0.25rem;
grid-column: 1 / -1;
}
.fvtt-cthulhu-eternal .tab.protagonist-skills .main-div .skills .skill { .fvtt-cthulhu-eternal .tab.protagonist-skills .main-div .skills .skill {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -918,6 +924,20 @@ i.fvtt-cthulhu-eternal {
min-width: 1.8rem; min-width: 1.8rem;
max-width: 1.8rem; max-width: 1.8rem;
} }
.fvtt-cthulhu-eternal .tab.protagonist-equipment .main-div .armors .armor .equipped-toggle {
font-size: 1rem;
min-width: 1.4rem;
max-width: 1.4rem;
cursor: pointer;
color: #888;
text-align: center;
}
.fvtt-cthulhu-eternal .tab.protagonist-equipment .main-div .armors .armor .equipped-toggle.active {
color: #4caf50;
}
.fvtt-cthulhu-eternal .tab.protagonist-equipment .main-div .armors .armor .equipped-toggle:hover {
color: #2196f3;
}
.fvtt-cthulhu-eternal .tab.protagonist-equipment .main-div .armors .armor .protection { .fvtt-cthulhu-eternal .tab.protagonist-equipment .main-div .armors .armor .protection {
min-width: 5rem; min-width: 5rem;
max-width: 5rem; max-width: 5rem;
@@ -3147,8 +3167,16 @@ i.fvtt-cthulhu-eternal {
font-size: calc(var(--font-size-standard) * 1.2); font-size: calc(var(--font-size-standard) * 1.2);
text-shadow: 0 0 10px var(--color-shadow-primary); text-shadow: 0 0 10px var(--color-shadow-primary);
} }
.dice-roll .chat-actions { .dice-roll .skill-progress-notice {
display: flex; margin: 0.3rem 0 0;
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
font-style: italic;
color: var(--color-level-warning);
border-left: 3px solid var(--color-level-warning);
background: rgba(var(--color-level-warning-rgb, 180, 130, 0), 0.08);
}
.dice-roll .chat-actions { display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.375rem; gap: 0.375rem;
padding: 0.5rem; padding: 0.5rem;
@@ -3190,9 +3218,23 @@ i.fvtt-cthulhu-eternal {
.dice-roll .chat-actions .nudge-roll, .dice-roll .chat-actions .nudge-roll,
.dice-roll .chat-actions .damage-roll, .dice-roll .chat-actions .damage-roll,
.dice-roll .chat-actions .healing-roll, .dice-roll .chat-actions .healing-roll,
.dice-roll .chat-actions .opposed-roll { .dice-roll .chat-actions .opposed-roll,
.dice-roll .chat-actions .nudge-to-success {
display: none; display: none;
} }
.dice-roll .chat-actions .nudge-to-success {
width: auto;
padding: 0 0.5rem !important;
gap: 0.3rem;
font-size: 0.75rem;
font-weight: bold;
background: var(--color-level-success);
color: var(--color-light-1);
border-color: var(--color-level-success);
}
.dice-roll .chat-actions .nudge-to-success:hover {
filter: brightness(1.15);
}
.opposed-roll-result { .opposed-roll-result {
padding: 0.5rem; padding: 0.5rem;
background: rgba(0, 0, 0, 0.05); background: rgba(0, 0, 0, 0.05);
+67 -22
View File
@@ -159,60 +159,105 @@ Hooks.on('tokenActionHudCoreApiReady', async () => {
}) })
Hooks.on("renderChatMessageHTML", (message, html, data) => { Hooks.on("renderChatMessageHTML", (message, html, data) => {
const alreadyUsedLabel = game.i18n.localize("CTHULHUETERNAL.Label.alreadyUsed")
// Helper: disable all action buttons in the message
function disableChatActions(container) {
$(container).find(".chat-action-button").each((i, btn) => {
btn.setAttribute("disabled", "disabled")
btn.setAttribute("data-tooltip", alreadyUsedLabel)
btn.style.opacity = "0.5"
btn.style.cursor = "not-allowed"
btn.style.pointerEvents = "none"
})
}
// If an action was already used on this message, disable all buttons immediately
if (message.getFlag("fvtt-cthulhu-eternal", "actionUsed")) {
disableChatActions(html)
}
// Wrapper: click any action button → disable all + persist flag
function withActionLock(handler) {
return async (event) => {
const container = $(event.currentTarget).closest(".chat-message, .message-content, div[class]")[0] || html
disableChatActions(container)
message.setFlag("fvtt-cthulhu-eternal", "actionUsed", true).catch(() => { })
await handler(event)
}
}
// Affichage des boutons de jet de dés uniquement pour les joueurs // Affichage des boutons de jet de dés uniquement pour les joueurs
if (message.author.id === game.user.id || game.user.isGM) { if (message.author.id === game.user.id || game.user.isGM) {
$(html).find(".nudge-roll").each((i, btn) => { $(html).find(".nudge-roll").each((i, btn) => {
btn.style.display = "inline" btn.style.display = "inline"
}) })
$(html).find(".nudge-to-success").each((i, btn) => {
btn.style.display = "inline-flex"
})
$(html).find(".damage-roll").each((i, btn) => { $(html).find(".damage-roll").each((i, btn) => {
btn.style.display = "inline" btn.style.display = "inline"
}) })
$(html).find(".healing-roll").each((i, btn) => { $(html).find(".healing-roll").each((i, btn) => {
btn.style.display = "inline" btn.style.display = "inline"
}) })
$(html).find(".worn-weapon-check").each((i, btn) => {
btn.style.display = "inline"
})
if (game.user.isGM) { if (game.user.isGM) {
$(html).find(".opposed-roll").each((i, btn) => { $(html).find(".opposed-roll").each((i, btn) => {
btn.style.display = "inline" btn.style.display = "inline"
}) })
} }
$(html).find(".nudge-roll").click((event) => { $(html).find(".nudge-roll").click(withActionLock(() => {
CthulhuEternalUtils.nudgeRoll(message) CthulhuEternalUtils.nudgeRoll(message)
}) }))
$(html).find(".damage-roll").click((event) => { $(html).find(".nudge-to-success").click(withActionLock(() => {
CthulhuEternalUtils.nudgeToSuccess(message)
}))
$(html).find(".damage-roll").click(withActionLock((event) => {
let formula = $(event.currentTarget).data("roll-value") let formula = $(event.currentTarget).data("roll-value")
CthulhuEternalUtils.damageRoll(message, formula) CthulhuEternalUtils.damageRoll(message, formula)
}) }))
$(html).find(".healing-roll").click((event) => { $(html).find(".healing-roll").click(withActionLock(() => {
CthulhuEternalUtils.healingRoll(message) CthulhuEternalUtils.healingRoll(message)
}) }))
$(html).find(".san-loose").click((event) => { $(html).find(".worn-weapon-check").click(withActionLock(() => {
CthulhuEternalUtils.wornWeaponCheck(message)
}))
$(html).find(".san-loose").click(withActionLock((event) => {
CthulhuEternalUtils.applySANLoss(message, event) CthulhuEternalUtils.applySANLoss(message, event)
}) }))
$(html).find(".san-type").click((event) => { $(html).find(".san-type").click(withActionLock((event) => {
CthulhuEternalUtils.applySANType(message, event) CthulhuEternalUtils.applySANType(message, event)
}) }))
$(html).find(".opposed-roll").click((event) => { $(html).find(".opposed-roll").click(withActionLock((event) => {
CthulhuEternalUtils.opposedRollManagement(message, event) CthulhuEternalUtils.opposedRollManagement(message, event)
}) }))
} }
if (game.user.isGM) { if (game.user.isGM) {
$(html).find(".li-apply-wounds").each((i, btn) => { $(html).find(".li-apply-wounds").each((i, btn) => {
btn.style.display = "block" btn.style.display = "block"
}) })
$(html).find(".apply-wounds-btn").click((event) => { $(html).find(".apply-wounds-btn").click(withActionLock((event) => {
CthulhuEternalUtils.applyWounds(message, event) CthulhuEternalUtils.applyWounds(message, event)
}) }))
$(html).find(".apply-wounds-btn").hover( $(html).find(".apply-wounds-btn").hover(
function (event) { function (event) {
// Mouse enter - select the token // Mouse enter - highlight the token on the canvas
let combatantId = $(this).data("combatant-id") const tokenId = $(this).data("token-id")
if (combatantId && game.combat) { if (tokenId) {
let combatant = game.combat.combatants.get(combatantId) const token = canvas.tokens.get(tokenId)
if (combatant?.token) { if (token) token.control({ releaseOthers: true })
let token = canvas.tokens.get(combatant.token.id) return
if (token) {
token.control({ releaseOthers: true })
} }
// Legacy: resolve via combatant
const combatantId = $(this).data("combatant-id")
if (combatantId && game.combat) {
const combatant = game.combat.combatants.get(combatantId)
if (combatant?.token) {
const token = canvas.tokens.get(combatant.token.id)
if (token) token.control({ releaseOthers: true })
} }
} }
}, },
+24 -1
View File
@@ -24,6 +24,9 @@
"Settings": { "Settings": {
"era": "Select the era of your game", "era": "Select the era of your game",
"eraHint": "Select the era of your game", "eraHint": "Select the era of your game",
"nudge": "Allow roll modification (WP spend)",
"nudgeHint": "When enabled, players can spend Willpower Points to modify failed rolls. Disable to remove this optional rule.",
"nudgeDisabled": "Roll modification via Willpower Points is currently disabled.",
"Common": "Common", "Common": "Common",
"Classical": "Classical", "Classical": "Classical",
"Medieval": "Medieval", "Medieval": "Medieval",
@@ -571,6 +574,8 @@
} }
}, },
"Label": { "Label": {
"JunkWeapon": "This weapon is in 'Junk' state, -20% applied. Critical failure on a roll of 96-100",
"WornWeapon": "This weapon is 'Worn', a Luck roll will be performed.",
"noTarget": "No target selected", "noTarget": "No target selected",
"noDefenseRoll": "No defense roll available", "noDefenseRoll": "No defense roll available",
"opposedRoll": "Opposed Roll", "opposedRoll": "Opposed Roll",
@@ -606,6 +611,10 @@
"rollNudge": "Roll Nudge", "rollNudge": "Roll Nudge",
"rollDamage": "Roll Damage", "rollDamage": "Roll Damage",
"rollHealing": "Roll Healing", "rollHealing": "Roll Healing",
"wornWeaponCheck": "Check Weapon Condition",
"wornWeaponCheckTitle": "Worn Weapon Condition Check",
"wornWeaponCheckSuccess": "{weapon}: Luck roll succeeded ({roll}/50) - Weapon remains worn.",
"wornWeaponCheckFailure": "{weapon}: Luck roll failed ({roll}/50) - Weapon became junk!",
"result": "Result", "result": "Result",
"damageMessage": "Damage to apply", "damageMessage": "Damage to apply",
"lethalityRoll": "Lethality Roll", "lethalityRoll": "Lethality Roll",
@@ -616,7 +625,9 @@
"Weapon": "Weapon", "Weapon": "Weapon",
"ZeroWP": "Zero WP : Automatic failure (ie 0%)", "ZeroWP": "Zero WP : Automatic failure (ie 0%)",
"LowWP": "Low WP", "LowWP": "Low WP",
"notEnoughWP": "Not enough WP to perform this action",
"Exhausted": "Exhausted", "Exhausted": "Exhausted",
"wornWeaponWarning": "Worn weapon: Don't forget to make the luck roll after the attack!",
"creature": "Creature", "creature": "Creature",
"Rituals": "Rituals", "Rituals": "Rituals",
"Tomes": "Tomes", "Tomes": "Tomes",
@@ -727,6 +738,8 @@
"newMentalDisorder": "New Mental Disorder", "newMentalDisorder": "New Mental Disorder",
"newWeapon": "New Weapon", "newWeapon": "New Weapon",
"newArmor": "New Armor", "newArmor": "New Armor",
"equipped": "Equipped (click to unequip)",
"unequipped": "Not equipped (click to equip)",
"newInjury": "New Injury", "newInjury": "New Injury",
"newGear": "New Gear", "newGear": "New Gear",
"newArcane": "New Arcane", "newArcane": "New Arcane",
@@ -744,6 +757,12 @@
"unconscious": "Unconscious", "unconscious": "Unconscious",
"dying": "Dying", "dying": "Dying",
"unconsciousWarning": "The Protagonist is unconscious. He cannot act until he has at least 3 HP.", "unconsciousWarning": "The Protagonist is unconscious. He cannot act until he has at least 3 HP.",
"stunnedWarning": "The Protagonist is stunned. He cannot act until he succeeds at a CON × 5 test.",
"deadWarning": "The Protagonist is dying. He will die if not treated within {con} minutes.",
"noSkills": "No skills. Add skills to this character.",
"noWeapons": "No weapons. Add weapons in edit mode.",
"noArmors": "No armor. Add armor in edit mode.",
"noGears": "No gear. Add gear in edit mode.",
"luck": "Luck", "luck": "Luck",
"Other": "Other", "Other": "Other",
"Skills": "Skills", "Skills": "Skills",
@@ -758,6 +777,7 @@
"lethalityLethal": "Lethal !!", "lethalityLethal": "Lethal !!",
"lethalityNotLethal": "Non-Lethal", "lethalityNotLethal": "Non-Lethal",
"WPSpent": "WP Spent", "WPSpent": "WP Spent",
"alreadyUsed": "Already used",
"targetMove": "Target Move", "targetMove": "Target Move",
"attackerState": "Attacker State", "attackerState": "Attacker State",
"targetSize": "Target Size", "targetSize": "Target Size",
@@ -793,7 +813,8 @@
"roll": "Roll", "roll": "Roll",
"applyNudge": "Apply", "applyNudge": "Apply",
"cancel": "Cancel", "cancel": "Cancel",
"nudgeRoll": "Nudge Roll" "nudgeRoll": "Nudge Roll",
"nudgeToSuccess": "Succeed at {score}% for {wp} WP"
}, },
"Tooltip": { "Tooltip": {
"sanBP": ">5 SAN lost in one roll, temporary insanity. If SAN less reaches BP = a Disorder unconscious Breaking and AND reset BP.", "sanBP": ">5 SAN lost in one roll, temporary insanity. If SAN less reaches BP = a Disorder unconscious Breaking and AND reset BP.",
@@ -814,6 +835,8 @@
"WrongEra": "The era of the item does not match the ear of the system", "WrongEra": "The era of the item does not match the ear of the system",
"NoSelectiveFireChoices": "Not enough ammo fo Selective Fire choices for this weapon.", "NoSelectiveFireChoices": "Not enough ammo fo Selective Fire choices for this weapon.",
"NoAmmo": "No more ammo for this weapon. ", "NoAmmo": "No more ammo for this weapon. ",
"WeaponBecameJunk": "Weapon {weapon} became junk!",
"noWeaponFound": "No weapon found.",
"noRollDataFound": "No roll data found", "noRollDataFound": "No roll data found",
"noActorFound": "No actor found for this item.", "noActorFound": "No actor found for this item.",
"noSanLossFound": "No SAN loss value found.", "noSanLossFound": "No SAN loss value found.",
+22 -3
View File
@@ -24,6 +24,9 @@
"Settings": { "Settings": {
"era": "Sélectionnez l'époque de votre jeu", "era": "Sélectionnez l'époque de votre jeu",
"eraHint": "L'époque détermine les compétences, les armes et les équipements disponibles pour votre protagoniste.", "eraHint": "L'époque détermine les compétences, les armes et les équipements disponibles pour votre protagoniste.",
"nudge": "Autoriser la modification des jets (dépense de PVO)",
"nudgeHint": "Si activé, les joueurs peuvent dépenser des Points de Volonté pour modifier leurs jets ratés. Désactivez pour supprimer cette règle optionnelle.",
"nudgeDisabled": "La modification des jets via les Points de Volonté est actuellement désactivée.",
"Common": "Commun", "Common": "Commun",
"Classical": "Classique", "Classical": "Classique",
"Medieval": "Médiéval", "Medieval": "Médiéval",
@@ -565,6 +568,8 @@
} }
}, },
"Label": { "Label": {
"JunkWeapon": "Cette arme est 'Défectueuse'', -20% appliqué. Échec critique sur un jet de 96-100",
"WornWeapon": "Cette arme est 'Usée', un jet de Chance sera effectué.",
"noTarget": "Aucune cible sélectionnée", "noTarget": "Aucune cible sélectionnée",
"noDefenseRoll": "Aucun jet de défense existant", "noDefenseRoll": "Aucun jet de défense existant",
"opposedRoll": "Jet opposé", "opposedRoll": "Jet opposé",
@@ -600,6 +605,10 @@
"rollNudge": "Modifier le jet", "rollNudge": "Modifier le jet",
"rollDamage": "Jet de dégâts", "rollDamage": "Jet de dégâts",
"rollHealing": "Jet de soin", "rollHealing": "Jet de soin",
"wornWeaponCheck": "Vérifier l'état de l'arme",
"wornWeaponCheckTitle": "Vérification d'état d'arme usée",
"wornWeaponCheckSuccess": "{weapon} : Jet de chance réussi ({roll}/50) - L'arme reste usée.",
"wornWeaponCheckFailure": "{weapon} : Jet de chance échoué ({roll}/50) - L'arme devient défectueuse !",
"result": "Resultat", "result": "Resultat",
"damageMessage": "Dégâts à appliquer", "damageMessage": "Dégâts à appliquer",
"lethalityRoll": "Jet de Létalité", "lethalityRoll": "Jet de Létalité",
@@ -610,8 +619,8 @@
"Weapon": "Arme", "Weapon": "Arme",
"ZeroWP": "PVO à 0 : Echec automatique (ie 0%)", "ZeroWP": "PVO à 0 : Echec automatique (ie 0%)",
"LowWP": "PVO faibles", "LowWP": "PVO faibles",
"Exhausted": "Epuisé", "notEnoughWP": "PVO insuffisants pour effectuer cette action",
"creature": "Créature", "Exhausted": "Epuisé", "wornWeaponWarning": "Arme usée : N'oubliez pas de faire le jet de chance après l'attaque !", "creature": "Créature",
"Rituals": "Rituels", "Rituals": "Rituels",
"Tomes": "Ouvrages", "Tomes": "Ouvrages",
"otherBenefits": "Autres avantages", "otherBenefits": "Autres avantages",
@@ -721,6 +730,8 @@
"newMentalDisorder": "Nouveau Trouble mental", "newMentalDisorder": "Nouveau Trouble mental",
"newWeapon": "Nouvelle Arme", "newWeapon": "Nouvelle Arme",
"newArmor": "Nouvelle Armure", "newArmor": "Nouvelle Armure",
"equipped": "Équipée (cliquer pour déséquiper)",
"unequipped": "Non équipée (cliquer pour équiper)",
"newInjury": "Nouvelle Blessure", "newInjury": "Nouvelle Blessure",
"newGear": "Nouvel Equipement", "newGear": "Nouvel Equipement",
"newArcane": "Nouvel Arcane", "newArcane": "Nouvel Arcane",
@@ -740,6 +751,10 @@
"stunnedWarning": "Votre protagoniste est étourdi. Il ne peut pas agir tant qu'il n'a pas réussi un test de CON x 5.", "stunnedWarning": "Votre protagoniste est étourdi. Il ne peut pas agir tant qu'il n'a pas réussi un test de CON x 5.",
"deadWarning": "Votre protagoniste est mourrant. Il mourra s'il n'est pas soigné dans les {con} minutes", "deadWarning": "Votre protagoniste est mourrant. Il mourra s'il n'est pas soigné dans les {con} minutes",
"unconsciousWarning": "Votre protagoniste est inconscient. Il ne peut pas agir tant qu'il n'a pas atteint 3 PV.", "unconsciousWarning": "Votre protagoniste est inconscient. Il ne peut pas agir tant qu'il n'a pas atteint 3 PV.",
"noSkills": "Aucune compétence. Ajoutez des compétences à ce personnage.",
"noWeapons": "Aucune arme. Ajoutez des armes en mode édition.",
"noArmors": "Aucune armure. Ajoutez une armure en mode édition.",
"noGears": "Aucun équipement. Ajoutez de l'équipement en mode édition.",
"luck": "Chance", "luck": "Chance",
"Other": "Autre", "Other": "Autre",
"Skills": "Compétences", "Skills": "Compétences",
@@ -753,6 +768,7 @@
"lethalityLethal": "Létal !!", "lethalityLethal": "Létal !!",
"lethalityNotLethal": "Non létal", "lethalityNotLethal": "Non létal",
"WPSpent": "PVO dépensés", "WPSpent": "PVO dépensés",
"alreadyUsed": "Déjà utilisé",
"targetMove": "Mouvement de la cible", "targetMove": "Mouvement de la cible",
"attackerState": "Etat de l'attaquant", "attackerState": "Etat de l'attaquant",
"targetSize": "Taille de la cible", "targetSize": "Taille de la cible",
@@ -789,7 +805,8 @@
"roll": "Jet", "roll": "Jet",
"applyNudge": "Lancer", "applyNudge": "Lancer",
"cancel": "Annuler", "cancel": "Annuler",
"nudgeRoll": "Modifier le jet" "nudgeRoll": "Modifier le jet",
"nudgeToSuccess": "Réussir à {score}% pour {wp} PVO"
}, },
"Tooltip": { "Tooltip": {
"sanBP": "Perte de 5+ SAN en 1 jet : folie temporaire. SI la SAN atteint le PR : trouble mental, perte de conscience et reset du PR.", "sanBP": "Perte de 5+ SAN en 1 jet : folie temporaire. SI la SAN atteint le PR : trouble mental, perte de conscience et reset du PR.",
@@ -810,6 +827,8 @@
"WrongEra": "L'époque de l'item ne correspond pas à celle du jeu en cours.", "WrongEra": "L'époque de l'item ne correspond pas à celle du jeu en cours.",
"NoSelectiveFireChoices": "Aucune option de tir sélectif n'est disponible pour cette arme : pas assez de munitions.", "NoSelectiveFireChoices": "Aucune option de tir sélectif n'est disponible pour cette arme : pas assez de munitions.",
"NoAmmo": "Aucune munition disponible pour cette arme.", "NoAmmo": "Aucune munition disponible pour cette arme.",
"WeaponBecameJunk": "L'arme {weapon} est devenue défectueuse !",
"noWeaponFound": "Aucune arme trouvée.",
"noRollDataFound": "Aucune donnée de jet trouvée.", "noRollDataFound": "Aucune donnée de jet trouvée.",
"noActorFound": "Aucun protagoniste trouvé.", "noActorFound": "Aucun protagoniste trouvé.",
"noSanLossFound": "Aucune valeur de perte de SAN trouvée.", "noSanLossFound": "Aucune valeur de perte de SAN trouvée.",
+12 -28
View File
@@ -1,6 +1,4 @@
// System Module Imports // System Module Imports
import { Utils } from './utils.js'
import { SYSTEM } from "../../config/system.mjs"
import CthulhuEternalUtils from '../../utils.mjs' import CthulhuEternalUtils from '../../utils.mjs'
export let ActionHandler = null export let ActionHandler = null
@@ -17,8 +15,7 @@ Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => {
* @param {array} groupIds * @param {array} groupIds
*/ */
async buildSystemActions(groupIds) { async buildSystemActions(groupIds) {
// Set actor and token variables // this.actor and this.actors are provided by the base class (TAH Core v2+)
this.actors = (!this.actor) ? this._getActors() : [this.actor]
this.actorType = this.actor?.type this.actorType = this.actor?.type
// Set items variable // Set items variable
@@ -28,9 +25,11 @@ Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => {
this.items = items this.items = items
} }
if (this.actor) {
if (this.actorType !== 'vehicle') { if (this.actorType !== 'vehicle') {
this.#buildCharacterActions() this.#buildCharacterActions()
} else if (!this.actor) { }
} else {
this.#buildMultipleTokenActions() this.#buildMultipleTokenActions()
} }
} }
@@ -201,9 +200,9 @@ Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => {
// Build alpha sorted skill list // Build alpha sorted skill list
let skills = this.actor.items.filter(i => i.type === 'skill') let skills = this.actor.items.filter(i => i.type === 'skill')
skills = skills.sort((a, b) => a.name.localeCompare(b.name)) skills = skills.sort((a, b) => a.name.localeCompare(b.name))
console.log('Building skills actions for skills:', skills)
for (const skill of skills) { for (const skill of skills) {
console.log('Processing skill item:', skill) //console.log('Processing skill item:', skill)
if (skill.system.skillTotal > 0) { if (skill.system.skillTotal > 0) {
const tooltip = { const tooltip = {
content: String(skill.system.skillTotal), content: String(skill.system.skillTotal),
@@ -224,8 +223,8 @@ Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => {
async buildEquipment() { async buildEquipment() {
let era = game.settings.get("fvtt-cthulhu-eternal", "settings-era") let era = game.settings.get("fvtt-cthulhu-eternal", "settings-era")
// const rituals = [] const weapons = this.actor.items.filter(i => i.type === 'weapon')
for (const item of this.actor.items) { for (const item of weapons) {
// Push the weapon name as a new group // Push the weapon name as a new group
const groupData = { const groupData = {
id: `weapons_${item._id}`, id: `weapons_${item._id}`,
@@ -233,16 +232,14 @@ Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => {
type: 'system' type: 'system'
} }
this.addGroup(groupData, { id: 'weapons', type: 'system' }, true) this.addGroup(groupData, { id: 'weapons', type: 'system' }, true)
if (item.type === 'weapon') {
let skill = CthulhuEternalUtils.getWeaponSkill(this.actor, item, era) let skill = CthulhuEternalUtils.getWeaponSkill(this.actor, item, era)
item.skillTotal = skill ? skill.system.skillTotal : 0 item.skillTotal = skill ? skill.system.skillTotal : 0
let weapons = [] let weaponActions = []
const tooltip = { const tooltip = {
content: String(item.skillTotal), content: String(item.skillTotal),
direction: 'LEFT' direction: 'LEFT'
} }
console.log('Weapon skill total for', item.name, 'is', item.skillTotal, item._id) weaponActions.push({
weapons.push({
name: `${item.name} (${item.skillTotal})`, name: `${item.name} (${item.skillTotal})`,
id: `weapon_${item._id}`, id: `weapon_${item._id}`,
info1: this.#showValue() ? { text: tooltip.content } : null, info1: this.#showValue() ? { text: tooltip.content } : null,
@@ -260,7 +257,7 @@ Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => {
direction: 'LEFT' direction: 'LEFT'
} }
if (item.system.damage !== '') { if (item.system.damage !== '') {
weapons.push({ weaponActions.push({
name: `${coreModule.api.Utils.i18n('CTHULHUETERNAL.Label.Damage')} (${damage})`, name: `${coreModule.api.Utils.i18n('CTHULHUETERNAL.Label.Damage')} (${damage})`,
id: `damage_${item._id}`, id: `damage_${item._id}`,
info1: this.#showValue() ? { text: damageTooltip.content } : null, info1: this.#showValue() ? { text: damageTooltip.content } : null,
@@ -268,23 +265,10 @@ Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => {
tooltip: damageTooltip tooltip: damageTooltip
}) })
} }
console.log('Adding weapon actions for', item.name, weapons) await this.addActions(weaponActions, {
await this.addActions(weapons, {
id: `weapons_${item._id}`, id: `weapons_${item._id}`,
type: 'system' type: 'system'
}) })
}/* else if (item.type === 'ritual') {
rituals.push({
name: item.name,
id: item._id,
encodedValue: ['rituals', item.name].join(this.delimiter)
})
} */
/* await this.addActions(rituals, {
id: 'rituals',
type: 'system'
}) */
} }
} }
+1 -1
View File
@@ -15,7 +15,7 @@ export const CORE_MODULE = {
/** /**
* Core module version required by the system module * Core module version required by the system module
*/ */
export const REQUIRED_CORE_MODULE_VERSION = '2.0' export const REQUIRED_CORE_MODULE_VERSION = '2'
/** /**
* Action types * Action types
+9 -13
View File
@@ -1,6 +1,3 @@
import { SYSTEM } from "../../config/system.mjs"
export let RollHandler = null export let RollHandler = null
Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => { Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => {
@@ -66,7 +63,6 @@ Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => {
* @param {string} actionId The actionId * @param {string} actionId The actionId
*/ */
async #handleAction(event, actor, token, actionTypeId, actionId) { async #handleAction(event, actor, token, actionTypeId, actionId) {
console.log('Handling action', actionId, 'of type', actionTypeId, 'for actor', actor.name)
switch (actionTypeId) { switch (actionTypeId) {
case 'characteristics': case 'characteristics':
await this.#handleCharacteristicsAction(event, actor, actionId) await this.#handleCharacteristicsAction(event, actor, actionId)
@@ -100,19 +96,17 @@ Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => {
* @param {string} actionId The action id * @param {string} actionId The action id
*/ */
async #handleCharacteristicsAction(event, actor, actionId) { async #handleCharacteristicsAction(event, actor, actionId) {
let rollType
if (actionId === 'wp' || actionId === 'hp') return if (actionId === 'wp' || actionId === 'hp') return
if (actionId.includes('_add') || actionId.includes('_subtract')) { if (actionId.includes('_add') || actionId.includes('_subtract')) {
const attr = actionId.split('_')[0] const attr = actionId.split('_')[0]
const action = actionId.split('_')[1] const action = actionId.split('_')[1]
console.log('Updating', attr, 'with action', action)
const update = {} const update = {}
update.system = {} update.system = {}
update.system[attr] = {} update.system[attr] = {}
update.system[attr].value = action === 'add' ? this.actor.system[attr].value + 1 : this.actor.system[attr].value - 1 update.system[attr].value = action === 'add' ? actor.system[attr].value + 1 : actor.system[attr].value - 1
if (update.system[attr].value > this.actor.system[attr].max || update.system[attr].value < this.actor.system[attr].min) return if (update.system[attr].value > actor.system[attr].max || update.system[attr].value < actor.system[attr].min) return
return await this.actor.update(update) return await actor.update(update)
} }
if (actionId === 'san') { if (actionId === 'san') {
let item = foundry.utils.duplicate(actor.system.san) let item = foundry.utils.duplicate(actor.system.san)
@@ -155,7 +149,8 @@ Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => {
* @param {string} actionId The action id * @param {string} actionId The action id
*/ */
async #handleWeaponsAction(event, actor, actionId) { async #handleWeaponsAction(event, actor, actionId) {
let weapon = actor.items.find(i => i.type === 'weapon' && i.id === actionId) const weapon = actor.items.find(i => i.type === 'weapon' && i.id === actionId)
if (!weapon) return ui.notifications.warn(`Weapon not found for action id '${actionId}'`)
weapon.damageFormula = weapon.system.damage weapon.damageFormula = weapon.system.damage
weapon.damageBonus = actor.system.damageBonus weapon.damageBonus = actor.system.damageBonus
await actor.system.roll('weapon', weapon) await actor.system.roll('weapon', weapon)
@@ -169,7 +164,8 @@ Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => {
* @param {string} actionId The action id * @param {string} actionId The action id
*/ */
async #handleDamageAction(event, actor, actionId) { async #handleDamageAction(event, actor, actionId) {
let weapon = actor.items.find(i => i.type === 'weapon' && i.id === actionId) const weapon = actor.items.find(i => i.type === 'weapon' && i.id === actionId)
if (!weapon) return ui.notifications.warn(`Weapon not found for action id '${actionId}'`)
weapon.damageFormula = weapon.system.damage weapon.damageFormula = weapon.system.damage
weapon.damageBonus = actor.system.damageBonus weapon.damageBonus = actor.system.damageBonus
await actor.system.roll('damage', weapon) await actor.system.roll('damage', weapon)
@@ -183,13 +179,13 @@ Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => {
* @param {string} actionId The action id * @param {string} actionId The action id
*/ */
async #handleLethalityAction(event, actor, actionId) { async #handleLethalityAction(event, actor, actionId) {
const item = await this.actor.items.get(actionId) const item = actor.items.get(actionId)
if (item.system.damage !== '' && event.ctrlKey) { if (item.system.damage !== '' && event.ctrlKey) {
const isLethal = !item.system.isLethal const isLethal = !item.system.isLethal
await item.update({ 'system.isLethal': isLethal }) await item.update({ 'system.isLethal': isLethal })
} else { } else {
const options = { const options = {
actor: this.actor, actor,
rollType: 'lethality', rollType: 'lethality',
key: item.system.lethality, key: item.system.lethality,
item item
+2 -9
View File
@@ -1,7 +1,6 @@
// System Module Imports // System Module Imports
import { ActionHandler } from './action-handler.js' import { ActionHandler } from './action-handler.js'
import { RollHandler as Core } from './roll-handler.js' import { RollHandler as Core } from './roll-handler.js'
import { MODULE } from './constants.js'
import { DEFAULTS } from './defaults.js' import { DEFAULTS } from './defaults.js'
import * as systemSettings from './settings.js' import * as systemSettings from './settings.js'
@@ -79,14 +78,8 @@ Hooks.once('tokenActionHudCoreApiReady', async (coreModule) => {
* @returns {object} The TAH system styles * @returns {object} The TAH system styles
*/ */
registerStyles() { registerStyles() {
return { // No system-specific styles; use the core styles only
template: { return {}
class: 'tah-style-template-style', // The class to add to first DIV element
file: 'tah-template-style', // The file without the css extension
moduleId: MODULE.ID, // The module ID
name: 'Template Style' // The name to display in the Token Action HUD Core 'Style' module setting
}
}
} }
} }
}) })
@@ -140,7 +140,6 @@ export default class CthulhuEternalActorSheet extends HandlebarsApplicationMixin
_onDragOver(event) {} _onDragOver(event) {}
async _onDropItem(item) { async _onDropItem(item) {
console.log("Dropped item", item)
let itemData = item.toObject() let itemData = item.toObject()
await this.document.createEmbeddedDocuments("Item", [itemData], { renderSheet: false }) await this.document.createEmbeddedDocuments("Item", [itemData], { renderSheet: false })
} }
@@ -160,15 +159,12 @@ export default class CthulhuEternalActorSheet extends HandlebarsApplicationMixin
} }
static #onUpdateCheckboxArray(event, target) { static #onUpdateCheckboxArray(event, target) {
console.log("Update checkbox array", event, target)
let arrayName = target.dataset.name let arrayName = target.dataset.name
let arrayIdx = Number(target.dataset.index) let arrayIdx = Number(target.dataset.index)
let dataPath = `system.san.${arrayName}` let dataPath = `system.san.${arrayName}`
let tab = foundry.utils.duplicate(this.document.system.san[arrayName]) let tab = foundry.utils.duplicate(this.document.system.san[arrayName])
tab[arrayIdx] = target.checked tab[arrayIdx] = target.checked
this.actor.update( { [dataPath]: tab } ) this.actor.update( { [dataPath]: tab } )
// Dump
console.log("Array name", arrayName, arrayIdx, target.checked, dataPath)
} }
/** /**
@@ -162,7 +162,7 @@ export default class CthulhuEternalCreatureSheet extends CthulhuEternalActorShee
async _onDrop(event) { async _onDrop(event) {
if (!this.isEditable || !this.isEditMode) return if (!this.isEditable || !this.isEditMode) return
const data = TextEditor.getDragEventData(event) const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event)
// Handle different data types // Handle different data types
switch (data.type) { switch (data.type) {
@@ -13,6 +13,7 @@ export default class CthulhuEternalProtagonistSheet extends CthulhuEternalActorS
}, },
actions: { actions: {
setBP: CthulhuEternalProtagonistSheet.#onSetBP, setBP: CthulhuEternalProtagonistSheet.#onSetBP,
toggleEquipped: CthulhuEternalProtagonistSheet.#onToggleEquipped,
createGear: CthulhuEternalProtagonistSheet.#onCreateGear, createGear: CthulhuEternalProtagonistSheet.#onCreateGear,
createArmor: CthulhuEternalProtagonistSheet.#onCreateArmor, createArmor: CthulhuEternalProtagonistSheet.#onCreateArmor,
createWeapon: CthulhuEternalProtagonistSheet.#onCreateWeapon, createWeapon: CthulhuEternalProtagonistSheet.#onCreateWeapon,
@@ -155,6 +156,12 @@ export default class CthulhuEternalProtagonistSheet extends CthulhuEternalActorS
this.document.system.setBP() this.document.system.setBP()
} }
static #onToggleEquipped(event, target) {
const itemId = target.dataset.itemId
const item = this.document.items.get(itemId)
if (item) item.update({ "system.equipped": !item.system.equipped })
}
static #onCreateGear(event, target) { static #onCreateGear(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("CTHULHUETERNAL.Label.newGear"), type: "gear" }]) this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("CTHULHUETERNAL.Label.newGear"), type: "gear" }])
} }
@@ -257,7 +264,7 @@ export default class CthulhuEternalProtagonistSheet extends CthulhuEternalActorS
async _onDrop(event) { async _onDrop(event) {
if (!this.isEditable || !this.isEditMode) return if (!this.isEditable || !this.isEditMode) return
const data = TextEditor.getDragEventData(event) const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event)
// Handle different data types // Handle different data types
switch (data.type) { switch (data.type) {
+1 -2
View File
@@ -32,7 +32,6 @@ export default class CthulhuEternalSkillSheet extends CthulhuEternalItemSheet {
} }
static async #onRollProgress(event, target) { static async #onRollProgress(event, target) {
console.log("Rolling progress for skill", this, event, target)
if (this.actor) { if (this.actor) {
const roll = await new Roll("1d4").evaluate() const roll = await new Roll("1d4").evaluate()
if (roll) { if (roll) {
@@ -41,7 +40,7 @@ export default class CthulhuEternalSkillSheet extends CthulhuEternalItemSheet {
user: game.user.id, user: game.user.id,
speaker: ChatMessage.getSpeaker({ actor: this.actor }), speaker: ChatMessage.getSpeaker({ actor: this.actor }),
content: `<div class="progress-roll">${game.i18n.localize("CTHULHUETERNAL.Label.skillProgress")} - ${this.document.name} +${roll.total}</div>`, content: `<div class="progress-roll">${game.i18n.localize("CTHULHUETERNAL.Label.skillProgress")} - ${this.document.name} +${roll.total}</div>`,
type: CONST.CHAT_MESSAGE_TYPES.ROLL, type: CONST.CHAT_MESSAGE_STYLES.ROLL,
roll: roll, roll: roll,
}; };
await ChatMessage.create(chatData); await ChatMessage.create(chatData);
+3 -3
View File
@@ -81,8 +81,8 @@ export default class CthulhuEternalVehicleSheet extends CthulhuEternalActorSheet
break break
case "description": case "description":
context.tab = context.tabs.description context.tab = context.tabs.description
context.enrichedDescription = await TextEditor.enrichHTML(doc.system.description, { async: true }) context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.description, { async: true })
context.enrichedNotes = await TextEditor.enrichHTML(doc.system.notes, { async: true }) context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.notes, { async: true })
break break
} }
return context return context
@@ -104,7 +104,7 @@ export default class CthulhuEternalVehicleSheet extends CthulhuEternalActorSheet
async _onDrop(event) { async _onDrop(event) {
if (!this.isEditable || !this.isEditMode) return if (!this.isEditable || !this.isEditMode) return
const data = TextEditor.getDragEventData(event) const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event)
// Handle different data types // Handle different data types
switch (data.type) { switch (data.type) {
+6 -1
View File
@@ -42,6 +42,12 @@ export default class CthulhuEternalActor extends Actor {
type: CONST.CHAT_MESSAGE_STYLES.OTHER type: CONST.CHAT_MESSAGE_STYLES.OTHER
}) })
} }
if (this.type === "protagonist" && changed?.system?.hp !== undefined) {
const hp = this.system.hp
this.toggleStatusEffect("dead", { active: !!hp.dead })
this.toggleStatusEffect("unconscious", { active: !!hp.unconscious && !hp.dead })
this.toggleStatusEffect("stun", { active: !!hp.stunned && !hp.dead && !hp.unconscious })
}
return super._onUpdate(changed, options, userId) return super._onUpdate(changed, options, userId)
} }
@@ -69,7 +75,6 @@ export default class CthulhuEternalActor extends Actor {
if (this.system.hp.value !== hp) { if (this.system.hp.value !== hp) {
this.update({ "system.hp.value": hp }) this.update({ "system.hp.value": hp })
} }
console.log("Applying wounds", { woundData, totalArmor, effectiveWounds })
// Chat message for GM only // Chat message for GM only
if (game.user.isGM) { if (game.user.isGM) {
let armorText = totalArmor > 0 ? game.i18n.format("CTHULHUETERNAL.Chat.armorAbsorbed", { armor: totalArmor }) : game.i18n.localize("CTHULHUETERNAL.Chat.noArmor") let armorText = totalArmor > 0 ? game.i18n.format("CTHULHUETERNAL.Chat.armorAbsorbed", { armor: totalArmor }) : game.i18n.localize("CTHULHUETERNAL.Chat.noArmor")
+62 -40
View File
@@ -139,11 +139,19 @@ export default class CthulhuEternalRoll extends Roll {
weapon.system.killRadius = choice.killRadius // Override kill radius weapon.system.killRadius = choice.killRadius // Override kill radius
} }
let combatants = [] let targets = []
if (game?.combat?.combatants) { if (game?.combat?.combatants) {
for (let c of game.combat.combatants) { for (let c of game.combat.combatants) {
if (c.actorid !== actor.id) { if (c.actorId !== actor.id) {
combatants.push({ id: c.id, name: c.name }) targets.push({ id: c.id, name: c.name, actorId: c.actorId, tokenId: c.token?.id })
}
}
}
// Fall back to scene tokens if not in combat
if (targets.length === 0 && canvas?.scene) {
for (let tokenDoc of canvas.scene.tokens) {
if (tokenDoc.actorId !== actor.id) {
targets.push({ name: tokenDoc.name, actorId: tokenDoc.actorId, tokenId: tokenDoc.id })
} }
} }
} }
@@ -162,12 +170,11 @@ export default class CthulhuEternalRoll extends Roll {
isLethal, isLethal,
ammoUsed: weapon?.ammoUsed || 0, ammoUsed: weapon?.ammoUsed || 0,
rollResult: lethalityRoll.total, rollResult: lethalityRoll.total,
combatants: combatants combatants: targets
} }
let flavor = await foundry.applications.handlebars.renderTemplate("systems/fvtt-cthulhu-eternal/templates/chat-lethal-damage.hbs", msgData)
let msg = await ChatMessage.create({ let msg = await ChatMessage.create({
user: game.user.id, user: game.user.id,
content: flavor, content: await foundry.applications.handlebars.renderTemplate("systems/fvtt-cthulhu-eternal/templates/chat-lethal-damage.hbs", msgData),
speaker: ChatMessage.getSpeaker({ actor: actor }), speaker: ChatMessage.getSpeaker({ actor: actor }),
}, { rollMode: options.rollMode, create: true }) }, { rollMode: options.rollMode, create: true })
await msg.setFlag("fvtt-cthulhu-eternal", "woundData", msgData) await msg.setFlag("fvtt-cthulhu-eternal", "woundData", msgData)
@@ -191,12 +198,11 @@ export default class CthulhuEternalRoll extends Roll {
formula, formula,
ammoUsed: weapon?.ammoUsed || 0, ammoUsed: weapon?.ammoUsed || 0,
rollResult: damageRoll.total, rollResult: damageRoll.total,
combatants: combatants combatants: targets
} }
let flavor = await foundry.applications.handlebars.renderTemplate("systems/fvtt-cthulhu-eternal/templates/chat-regular-damage.hbs", msgData)
let msg = await ChatMessage.create({ let msg = await ChatMessage.create({
user: game.user.id, user: game.user.id,
content: flavor, content: await foundry.applications.handlebars.renderTemplate("systems/fvtt-cthulhu-eternal/templates/chat-regular-damage.hbs", msgData),
speaker: ChatMessage.getSpeaker({ actor: actor }), speaker: ChatMessage.getSpeaker({ actor: actor }),
}, { rollMode: options.rollMode, create: true }) }, { rollMode: options.rollMode, create: true })
await msg.setFlag("fvtt-cthulhu-eternal", "woundData", msgData) await msg.setFlag("fvtt-cthulhu-eternal", "woundData", msgData)
@@ -254,7 +260,8 @@ export default class CthulhuEternalRoll extends Roll {
let formula = "1d100" let formula = "1d100"
let hasModifier = true let hasModifier = true
let hasMultiplier = false let hasMultiplier = false
options.isNudge = true const nudgeEnabled = game.settings.get("fvtt-cthulhu-eternal", "settings-nudge")
options.isNudge = nudgeEnabled
let actor = game.actors.get(options.actorId) let actor = game.actors.get(options.actorId)
let target = CthulhuEternalUtils.getTarget() let target = CthulhuEternalUtils.getTarget()
@@ -271,7 +278,7 @@ export default class CthulhuEternalRoll extends Roll {
case "san": case "san":
case "char": case "char":
options.initialScore = options.rollItem.targetScore options.initialScore = options.rollItem.targetScore
options.isNudge = (options.rollType !== "san") options.isNudge = nudgeEnabled && (options.rollType !== "san")
break break
case "resource": case "resource":
hasModifier = false hasModifier = false
@@ -290,12 +297,10 @@ export default class CthulhuEternalRoll extends Roll {
let era = game.settings.get("fvtt-cthulhu-eternal", "settings-era") let era = game.settings.get("fvtt-cthulhu-eternal", "settings-era")
if (era !== options.rollItem.system.settings) { if (era !== options.rollItem.system.settings) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.WrongEra")) ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.WrongEra"))
console.log("WP Wrong Era", era, options.rollItem.system.weaponType)
return return
} }
if (!SYSTEM.WEAPON_SKILL_MAPPING[era]?.[options.rollItem.system.weaponType]) { if (!SYSTEM.WEAPON_SKILL_MAPPING[era]?.[options.rollItem.system.weaponType]) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.NoWeaponType")) ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.NoWeaponType"))
console.log("WP Not found", era, options.rollItem.system.weaponType)
return return
} }
/*if (!target) { /*if (!target) {
@@ -310,7 +315,13 @@ export default class CthulhuEternalRoll extends Roll {
options.rollItem = CthulhuEternalUtils.getWeaponSkill(actor, options.rollItem, era) options.rollItem = CthulhuEternalUtils.getWeaponSkill(actor, options.rollItem, era)
options.initialScore = options.rollItem.system.skillTotal options.initialScore = options.rollItem.system.skillTotal
console.log("WEAPON", era, options.rollItem) if (options.weapon.system.state === "junk") {
options.isJunk = true
options.initialScore = Math.max(0, options.initialScore - 20) // -20% for junk weapons
}
if (options.weapon.system.state === "worn") {
options.isWorn = true
}
} }
break break
default: default:
@@ -318,7 +329,7 @@ export default class CthulhuEternalRoll extends Roll {
break break
} }
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes); //Object.fromEntries(Object.entries(CONFIG.Dice.rollModes).map(([key, value]) => [key, game.i18n.localize(value)])) const rollModes = foundry.utils.duplicate(CONFIG.ChatMessage.modes ?? CONFIG.Dice.rollModes)
const fieldRollMode = new foundry.data.fields.StringField({ const fieldRollMode = new foundry.data.fields.StringField({
choices: rollModes, choices: rollModes,
blank: false, blank: false,
@@ -341,6 +352,8 @@ export default class CthulhuEternalRoll extends Roll {
targetScore: options.initialScore, targetScore: options.initialScore,
isLowWP: options.isLowWP, isLowWP: options.isLowWP,
isZeroWP: options.isZeroWP, isZeroWP: options.isZeroWP,
isJunk: options.isJunk,
isWorn: options.isWorn,
isExhausted: options.isExhausted, isExhausted: options.isExhausted,
enableHand: options.rollItem.enableHand, enableHand: options.rollItem.enableHand,
enableStowed: options.rollItem.enableStowed, enableStowed: options.rollItem.enableStowed,
@@ -434,8 +447,6 @@ export default class CthulhuEternalRoll extends Roll {
rollData.rollMode = rollContext.visibility rollData.rollMode = rollContext.visibility
// Update target score // Update target score
console.log("Rolldata", rollData)
if (options.rollType === "resource") { if (options.rollType === "resource") {
rollData.targetScore = options.initialScore * Number(rollContext.multiplier) rollData.targetScore = options.initialScore * Number(rollContext.multiplier)
} else { } else {
@@ -464,14 +475,15 @@ export default class CthulhuEternalRoll extends Roll {
const roll = new this(formula, options.data, rollData) const roll = new this(formula, options.data, rollData)
await roll.evaluate() await roll.evaluate()
roll.displayRollResult(roll, options, rollData) await roll.displayRollResult(roll, options, rollData)
if (Hooks.call("fvtt-cthulhu-eternal.Roll", options, rollData, roll) === false) return if (Hooks.call("fvtt-cthulhu-eternal.Roll", options, rollData, roll) === false) return
return roll return roll
} }
displayRollResult(formula, options, rollData) { async displayRollResult(formula, options, rollData) {
// Compute the result quality // Compute the result quality
let resultType = "failure" let resultType = "failure"
@@ -489,6 +501,11 @@ export default class CthulhuEternalRoll extends Roll {
resultType = "failureCritical" resultType = "failureCritical"
} }
} }
if (rollData.weapon?.system?.state === "junk" && this.total > 95) {
this.options.isDestroyed = true
rollData.isDestroyed = true
resultType = "failureCritical"
}
// As per the rules, a roll of 100 is always a failure, even if the target is above 100 // As per the rules, a roll of 100 is always a failure, even if the target is above 100
if (this.total === 100) { if (this.total === 100) {
resultType = "failureCritical" resultType = "failureCritical"
@@ -516,6 +533,16 @@ export default class CthulhuEternalRoll extends Roll {
} }
rollData.resultType = resultType rollData.resultType = resultType
// Compute WP cost to nudge a failure into a success (minimum roll needed = targetScore)
if (this.options.isFailure && options.isNudge && !this.options.isNudgedRoll && rollData.targetScore > 0 && this.total > rollData.targetScore) {
const actor = game.actors.get(options.actorId)
const wpAvailable = actor?.system?.wp?.value ?? 0
const wpCostToSucceed = Math.ceil((this.total - rollData.targetScore) / 5)
this.options.wpCostToSucceed = (wpCostToSucceed <= wpAvailable) ? wpCostToSucceed : 0
} else {
this.options.wpCostToSucceed = 0
}
this.options.isLowWP = rollData.isLowWP this.options.isLowWP = rollData.isLowWP
this.options.isZeroWP = rollData.isZeroWP this.options.isZeroWP = rollData.isZeroWP
this.options.isExhausted = rollData.isExhausted this.options.isExhausted = rollData.isExhausted
@@ -675,7 +702,19 @@ export default class CthulhuEternalRoll extends Roll {
* @param {boolean} [options.create=true] Whether to create the message. * @param {boolean} [options.create=true] Whether to create the message.
* @returns {Promise} - A promise that resolves when the message is created. * @returns {Promise} - A promise that resolves when the message is created.
*/ */
async toMessage(messageData = {}, { rollMode, create = true } = {}) { async toMessage(messageData = {}, { rollMode, messageMode, create = true } = {}) {
let rollData = this.options.rollData || this.options
let rollItem = this.options.rollItem
// Determine skill progression before calling super.toMessage so _getChatCardData picks it up
let skillMarkedForProgress = false
if (rollData.resultType?.includes("failure") && rollItem?.type === "skill") {
if (rollItem.system.diceEvolved && !rollItem.system.rollFailed && !rollItem.system.isAdversary) {
skillMarkedForProgress = true
}
}
this.options.skillMarkedForProgress = skillMarkedForProgress
let rollMsg = await super.toMessage( let rollMsg = await super.toMessage(
{ {
isFailure: this.resultType === "failure", isFailure: this.resultType === "failure",
@@ -685,34 +724,17 @@ export default class CthulhuEternalRoll extends Roll {
realDamage: this.realDamage, realDamage: this.realDamage,
...messageData, ...messageData,
}, },
{ rollMode: rollMode }, { messageMode: messageMode ?? rollMode },
) )
let rollData = this.options.rollData || this.options
let rollItem = this.options.rollItem
await rollMsg.setFlag("fvtt-cthulhu-eternal", "rollData", rollData) await rollMsg.setFlag("fvtt-cthulhu-eternal", "rollData", rollData)
// Manage the skill evolution if the roll is a failure // Apply skill rollFailed flag after the message is created
if (rollData.resultType.includes("failure") && rollItem.type === "skill") { if (skillMarkedForProgress) {
// Is the skill able to progress
if (rollItem.system.diceEvolved && !rollItem.system.rollFailed) {
// If the skill is not adversary, we can evolve it
if (!rollItem.system.isAdversary) {
rollItem.system.rollFailed = true
// Get the actor and update the skill
const actor = game.actors.get(rollData.actorId) const actor = game.actors.get(rollData.actorId)
await actor.updateEmbeddedDocuments("Item", [{ await actor.updateEmbeddedDocuments("Item", [{
_id: rollItem._id, _id: rollItem._id,
"system.rollFailed": true "system.rollFailed": true
}]) }])
// Create a chat message to inform the user
const flavor = `${rollItem.name} - ${game.i18n.localize("CTHULHUETERNAL.Label.skillFailed")}`
await ChatMessage.create({
user: game.user.id,
content: `<div class="cthulhu-eternal-roll"><p>${flavor}</p></div>`,
speaker: ChatMessage.getSpeaker({ actor: rollData.actor }),
}, { rollMode: rollData.rollMode, create: true })
}
}
} }
// If the roll is a SAN roll, we propose to select the SAN loss // If the roll is a SAN roll, we propose to select the SAN loss
+1
View File
@@ -12,6 +12,7 @@ export default class CthulhuEternalArmor extends foundry.abstract.TypeDataModel
schema.protection = new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 }) schema.protection = new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 })
schema.resourceLevel = new fields.NumberField({ required: true, initial: 0, min: 0 }) schema.resourceLevel = new fields.NumberField({ required: true, initial: 0, min: 0 })
schema.equipped = new fields.BooleanField({ required: true, initial: false })
return schema return schema
} }
+1 -1
View File
@@ -74,7 +74,7 @@ export default class CthulhuEternalCreature extends foundry.abstract.TypeDataMod
}) })
if (!roll) return null if (!roll) return null
await roll.toMessage({}, { rollMode: roll.options.rollMode }) await roll.toMessage({}, { messageMode: roll.options.rollMode })
} }
} }
+1 -2
View File
@@ -277,7 +277,6 @@ export default class CthulhuEternalProtagonist extends foundry.abstract.TypeData
template = "systems/fvtt-cthulhu-eternal/templates/chat-san-loss-none.hbs" template = "systems/fvtt-cthulhu-eternal/templates/chat-san-loss-none.hbs"
} }
console.log("CthulhuEternalProtagonist.applySANConsequences", rollData, updates, template)
let content = await foundry.applications.handlebars.renderTemplate(template, msgData) let content = await foundry.applications.handlebars.renderTemplate(template, msgData)
let msg = await ChatMessage.create({ let msg = await ChatMessage.create({
content: content, content: content,
@@ -402,7 +401,7 @@ export default class CthulhuEternalProtagonist extends foundry.abstract.TypeData
}) })
if (!roll) return null if (!roll) return null
await roll.toMessage({}, { rollMode: roll.options.rollMode }) await roll.toMessage({}, { messageMode: roll.options.rollMode })
} }
} }
-1
View File
@@ -54,7 +54,6 @@ export default class CthulhuEternalWeapon extends foundry.abstract.TypeDataModel
} }
isRanged() { isRanged() {
console.log("isRanged", this.weaponType, this)
return this.weaponType.match("ranged") return this.weaponType.match("ranged")
} }
+155 -21
View File
@@ -16,6 +16,14 @@ export default class CthulhuEternalUtils {
config: true, config: true,
onChange: _ => window.location.reload() onChange: _ => window.location.reload()
}); });
game.settings.register("fvtt-cthulhu-eternal", "settings-nudge", {
name: game.i18n.localize("CTHULHUETERNAL.Settings.nudge"),
hint: game.i18n.localize("CTHULHUETERNAL.Settings.nudgeHint"),
default: true,
scope: "world",
type: Boolean,
config: true
});
game.settings.register("fvtt-cthulhu-eternal", "roll-opposed-store", { game.settings.register("fvtt-cthulhu-eternal", "roll-opposed-store", {
name: "Roll Opposed Store", name: "Roll Opposed Store",
hint: "Whether to store opposed roll results for later use", hint: "Whether to store opposed roll results for later use",
@@ -118,9 +126,6 @@ export default class CthulhuEternalUtils {
return Object.keys(obj).length; return Object.keys(obj).length;
}) })
Handlebars.registerHelper('isEnabled', function (configKey) {
return game.settings.get("bol", configKey);
})
Handlebars.registerHelper('split', function (str, separator, keep) { Handlebars.registerHelper('split', function (str, separator, keep) {
return str.split(separator)[keep]; return str.split(separator)[keep];
}) })
@@ -164,7 +169,6 @@ export default class CthulhuEternalUtils {
return eval(expr); return eval(expr);
}) })
Handlebars.registerHelper('isOwnerOrGM', function (actor) { Handlebars.registerHelper('isOwnerOrGM', function (actor) {
console.log("Testing actor", actor.isOwner, game.userId)
return actor.isOwner || game.isGM; return actor.isOwner || game.isGM;
}) })
Handlebars.registerHelper('upperFirst', function (text) { Handlebars.registerHelper('upperFirst', function (text) {
@@ -382,8 +386,6 @@ export default class CthulhuEternalUtils {
loser = { actor: actor1, rollData: roll1.rollData, messageId: roll1.messageId } loser = { actor: actor1, rollData: roll1.rollData, messageId: roll1.messageId }
} }
console.log("Opposed roll result", winner, loser)
// Check if winner was attacking with a weapon that can apply damage // Check if winner was attacking with a weapon that can apply damage
let canApplyDamage = winner && winner.rollData?.weapon && winner.rollData.weapon.system let canApplyDamage = winner && winner.rollData?.weapon && winner.rollData.weapon.system
@@ -476,26 +478,130 @@ export default class CthulhuEternalUtils {
return return
} }
console.log("Damage roll data", rollData)
rollData.weapon.resultType = rollData.resultType // Keep the result type from the roll message rollData.weapon.resultType = rollData.resultType // Keep the result type from the roll message
rollData.weapon.selectiveFireChoice = rollData.selectiveFireChoice // Keep the selected fire choice from the roll message rollData.weapon.selectiveFireChoice = rollData.selectiveFireChoice // Keep the selected fire choice from the roll message
rollData.weapon.damageFormula = formula || rollData.weapon.system.damage rollData.weapon.damageFormula = formula || rollData.weapon.system.damage
actor.system.roll("damage", rollData.weapon) actor.system.roll("damage", rollData.weapon)
} }
static async wornWeaponCheck(rollMessage) {
let rollData = rollMessage.rolls[0]?.options?.rollData
if (!rollData) {
rollData = rollMessage.getFlag("fvtt-cthulhu-eternal", "rollData")
}
if (!rollData) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Label.noRollDataFound"))
return
}
let actor = game.actors.get(rollData.actorId)
if (!actor) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Label.noActorFound"))
return
}
let weapon = rollData.weapon
if (!weapon) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Label.noWeaponFound"))
return
}
// Lance un jet de chance (50%)
let luckRoll = new Roll("1d100")
await luckRoll.evaluate()
let isSuccess = luckRoll.total <= 50
let resultMsg = ""
if (isSuccess) {
// Succès - l'arme reste worn
resultMsg = game.i18n.format("CTHULHUETERNAL.Label.wornWeaponCheckSuccess", {
weapon: weapon.name,
roll: luckRoll.total
})
} else {
// Échec - l'arme devient junk
resultMsg = game.i18n.format("CTHULHUETERNAL.Label.wornWeaponCheckFailure", {
weapon: weapon.name,
roll: luckRoll.total
})
// Mettre à jour l'état de l'arme
await actor.updateEmbeddedDocuments("Item", [{
_id: weapon._id,
"system.state": "junk"
}])
ui.notifications.warn(game.i18n.format("CTHULHUETERNAL.Notifications.WeaponBecameJunk", { weapon: weapon.name }))
}
// Créer un message de chat avec le résultat
await ChatMessage.create({
user: game.user.id,
content: `<div class="cthulhu-eternal-roll">
<h4>${game.i18n.localize("CTHULHUETERNAL.Label.wornWeaponCheckTitle")}</h4>
<p>${resultMsg}</p>
</div>`,
speaker: ChatMessage.getSpeaker({ actor: actor }),
})
}
static async nudgeToSuccess(rollMessage) {
if (!game.settings.get("fvtt-cthulhu-eternal", "settings-nudge")) {
ui.notifications.warn(game.i18n.localize("CTHULHUETERNAL.Settings.nudgeDisabled"))
return
}
const rollOptions = rollMessage.rolls[0]?.options
if (!rollOptions) return
const actor = game.actors.get(rollOptions.actorId)
if (!actor) return
const rollTotal = rollMessage.rolls[0].total
const targetScore = rollOptions.rollData?.targetScore ?? rollOptions.targetScore
const wpCostToSucceed = Math.ceil((rollTotal - targetScore) / 5)
if (actor.system.wp.value < wpCostToSucceed) {
ui.notifications.warn(game.i18n.localize("CTHULHUETERNAL.Label.notEnoughWP"))
return
}
const dialogContext = foundry.utils.duplicate(rollOptions)
dialogContext.nudgedValue = targetScore
dialogContext.wpCost = wpCostToSucceed
dialogContext.isNudgedRoll = true
dialogContext.isNudge = false
const roll = new CthulhuEternalRoll(String(targetScore))
await roll.evaluate()
roll.options = dialogContext
roll.displayRollResult(roll, dialogContext, dialogContext.rollData)
roll.toMessage()
actor.system.modifyWP(-wpCostToSucceed)
// Original roll was a failure with skill progression — nudge succeeded, so unmark progression
if (rollOptions.skillMarkedForProgress && rollOptions.rollItem?._id) {
const skillItem = actor.items.get(rollOptions.rollItem._id)
if (skillItem) await skillItem.update({ "system.rollFailed": false })
}
await rollMessage.delete()
}
static async nudgeRoll(rollMessage) { static async nudgeRoll(rollMessage) {
if (!game.settings.get("fvtt-cthulhu-eternal", "settings-nudge")) {
ui.notifications.warn(game.i18n.localize("CTHULHUETERNAL.Settings.nudgeDisabled"))
return
}
let dialogContext = rollMessage.rolls[0]?.options let dialogContext = rollMessage.rolls[0]?.options
let actor = game.actors.get(dialogContext.actorId) let actor = game.actors.get(dialogContext.actorId)
const rollTotal = rollMessage.rolls[0].total
dialogContext.wpValue = actor.system.wp.value dialogContext.wpValue = actor.system.wp.value
dialogContext.rollResultIndex = rollMessage.rolls[0].total - 1 // Rule: 1 WP can decrease the roll by 15. Only decrease is allowed.
dialogContext.minValue = Math.max(rollMessage.rolls[0].total - (dialogContext.wpValue * 5), 1) dialogContext.minValue = Math.max(rollTotal - (dialogContext.wpValue * 5), 1)
dialogContext.maxValue = Math.min(rollMessage.rolls[0].total + (dialogContext.wpValue * 5), 100) dialogContext.maxValue = rollTotal
dialogContext.rollResultIndex = rollTotal - dialogContext.minValue // index of current value in nudgeOptions
dialogContext.wpCost = 0 dialogContext.wpCost = 0
dialogContext.nudgedValue = rollTotal // default: no change for the select: values from minValue to maxValue (current roll)
// Build options table for the select operator between minValue and maxValue
dialogContext.nudgeOptions = Array.from({ length: dialogContext.maxValue - dialogContext.minValue + 1 }, (_, i) => dialogContext.minValue + i) dialogContext.nudgeOptions = Array.from({ length: dialogContext.maxValue - dialogContext.minValue + 1 }, (_, i) => dialogContext.minValue + i)
console.log(dialogContext)
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-cthulhu-eternal/templates/nudge-dialog.hbs", dialogContext) const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-cthulhu-eternal/templates/nudge-dialog.hbs", dialogContext)
@@ -513,6 +619,9 @@ export default class CthulhuEternalUtils {
if (input.name) obj[input.name] = input.value if (input.name) obj[input.name] = input.value
return obj return obj
}, {}) }, {})
// Compute nudgedValue from the selected index (selectOptions uses 0-based indices for arrays)
dialogContext.nudgedValue = dialogContext.minValue + Number(output.modifiedValue)
dialogContext.wpCost = Math.ceil(Math.abs(rollTotal - dialogContext.nudgedValue) / 5)
return output return output
}, },
}, },
@@ -527,7 +636,7 @@ export default class CthulhuEternalUtils {
rejectClose: false, // Click on Close button will not launch an error rejectClose: false, // Click on Close button will not launch an error
render: (event, dialog) => { render: (event, dialog) => {
$(".nudged-score-select").change(event => { $(".nudged-score-select").change(event => {
dialogContext.nudgedValue = Number(event.target.value) + 1 dialogContext.nudgedValue = dialogContext.minValue + Number(event.target.value)
dialogContext.wpCost = Math.ceil(Math.abs(rollMessage.rolls[0].total - dialogContext.nudgedValue) / 5) dialogContext.wpCost = Math.ceil(Math.abs(rollMessage.rolls[0].total - dialogContext.nudgedValue) / 5)
$("#nudged-wp-cost").val(dialogContext.wpCost) $("#nudged-wp-cost").val(dialogContext.wpCost)
}) })
@@ -539,6 +648,11 @@ export default class CthulhuEternalUtils {
return return
} }
if (actor.system.wp.value < dialogContext.wpCost) {
ui.notifications.warn(game.i18n.localize("CTHULHUETERNAL.Label.notEnoughWP"))
return
}
const roll = new CthulhuEternalRoll(String(dialogContext.nudgedValue)) const roll = new CthulhuEternalRoll(String(dialogContext.nudgedValue))
await roll.evaluate() await roll.evaluate()
roll.options = dialogContext roll.options = dialogContext
@@ -549,6 +663,13 @@ export default class CthulhuEternalUtils {
actor.system.modifyWP(-dialogContext.wpCost) actor.system.modifyWP(-dialogContext.wpCost)
// If original roll had skill progression and nudge converts it to success, unmark progression
const nudgedTargetScore = dialogContext.rollData?.targetScore ?? dialogContext.targetScore
if (dialogContext.skillMarkedForProgress && dialogContext.nudgedValue <= nudgedTargetScore && dialogContext.rollItem?._id) {
const skillItem = actor.items.get(dialogContext.rollItem._id)
if (skillItem) await skillItem.update({ "system.rollFailed": false })
}
// Delete the initial roll message // Delete the initial roll message
await rollMessage.delete() await rollMessage.delete()
@@ -590,16 +711,29 @@ export default class CthulhuEternalUtils {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.noActorFound")) ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.noActorFound"))
return return
} }
console.log("Applying wounds", woundData)
// Remove the chat message
this.removeChatMessageId(message.id) this.removeChatMessageId(message.id)
// Get the targetted actorId from the button's data attribute // Resolve the target actor: prefer canvas token (works for both combat and scene tokens)
let targetCombatantId = event.currentTarget.dataset.combatantId const targetTokenId = event.currentTarget.dataset.tokenId
let combatant = game.combat.combatants.get(targetCombatantId) const targetActorId = event.currentTarget.dataset.actorId
let targetActor = combatant.token?.actor || game.actors.get(combatant.actorId) let targetActor
if (targetTokenId) {
const token = canvas.tokens.get(targetTokenId)
targetActor = token?.actor
}
if (!targetActor && targetActorId) {
targetActor = game.actors.get(targetActorId)
}
// Legacy fallback: combatant lookup
if (!targetActor) { if (!targetActor) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.noTargetActorFound") + targetCombatantId) const combatantId = event.currentTarget.dataset.combatantId
if (combatantId && game.combat) {
const combatant = game.combat.combatants.get(combatantId)
targetActor = combatant?.token?.actor || game.actors.get(combatant?.actorId)
}
}
if (!targetActor) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.noTargetActorFound"))
return return
} }
targetActor.applyWounds(woundData) targetActor.applyWounds(woundData)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000273 MANIFEST-000306
+7 -7
View File
@@ -1,7 +1,7 @@
2025/11/21-23:23:35.521610 7f21eeffd6c0 Recovering log #271 2026/05/13-20:38:21.073323 7ff670fed6c0 Recovering log #304
2025/11/21-23:23:35.612353 7f21eeffd6c0 Delete type=3 #269 2026/05/13-20:38:21.083446 7ff670fed6c0 Delete type=3 #302
2025/11/21-23:23:35.612430 7f21eeffd6c0 Delete type=0 #271 2026/05/13-20:38:21.083514 7ff670fed6c0 Delete type=0 #304
2025/11/21-23:35:34.796894 7f21ed7fa6c0 Level-0 table #276: started 2026/05/13-21:29:49.379063 7ff6637fe6c0 Level-0 table #309: started
2025/11/21-23:35:34.796917 7f21ed7fa6c0 Level-0 table #276: 0 bytes OK 2026/05/13-21:29:49.379189 7ff6637fe6c0 Level-0 table #309: 0 bytes OK
2025/11/21-23:35:34.806397 7f21ed7fa6c0 Delete type=0 #274 2026/05/13-21:29:49.386620 7ff6637fe6c0 Delete type=0 #307
2025/11/21-23:35:34.806600 7f21ed7fa6c0 Manual compaction at level-0 from '!items!4oyPRBWPBWAChrJP' @ 72057594037927935 : 1 .. '!items!zVFfp3o0G0Zg3Ia4' @ 0 : 0; will stop at (end) 2026/05/13-21:29:49.393683 7ff6637fe6c0 Manual compaction at level-0 from '!items!4oyPRBWPBWAChrJP' @ 72057594037927935 : 1 .. '!items!zVFfp3o0G0Zg3Ia4' @ 0 : 0; will stop at (end)
+7 -7
View File
@@ -1,7 +1,7 @@
2025/11/12-23:48:30.686758 7f4781ffb6c0 Recovering log #267 2026/05/13-13:05:01.465748 7ff6717ee6c0 Recovering log #300
2025/11/12-23:48:30.697449 7f4781ffb6c0 Delete type=3 #265 2026/05/13-13:05:01.518261 7ff6717ee6c0 Delete type=3 #298
2025/11/12-23:48:30.697507 7f4781ffb6c0 Delete type=0 #267 2026/05/13-13:05:01.518318 7ff6717ee6c0 Delete type=0 #300
2025/11/13-13:58:42.410831 7f4780bff6c0 Level-0 table #272: started 2026/05/13-13:57:22.815510 7ff6637fe6c0 Level-0 table #305: started
2025/11/13-13:58:42.410865 7f4780bff6c0 Level-0 table #272: 0 bytes OK 2026/05/13-13:57:22.815533 7ff6637fe6c0 Level-0 table #305: 0 bytes OK
2025/11/13-13:58:42.417362 7f4780bff6c0 Delete type=0 #270 2026/05/13-13:57:22.821566 7ff6637fe6c0 Delete type=0 #303
2025/11/13-13:58:42.434870 7f4780bff6c0 Manual compaction at level-0 from '!items!4oyPRBWPBWAChrJP' @ 72057594037927935 : 1 .. '!items!zVFfp3o0G0Zg3Ia4' @ 0 : 0; will stop at (end) 2026/05/13-13:57:22.821711 7ff6637fe6c0 Manual compaction at level-0 from '!items!4oyPRBWPBWAChrJP' @ 72057594037927935 : 1 .. '!items!zVFfp3o0G0Zg3Ia4' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000442 MANIFEST-000475
+7 -7
View File
@@ -1,7 +1,7 @@
2025/11/21-23:23:35.280848 7f21edffb6c0 Recovering log #440 2026/05/13-20:38:21.045516 7ff6717ee6c0 Recovering log #473
2025/11/21-23:23:35.368467 7f21edffb6c0 Delete type=3 #438 2026/05/13-20:38:21.056349 7ff6717ee6c0 Delete type=3 #471
2025/11/21-23:23:35.368552 7f21edffb6c0 Delete type=0 #440 2026/05/13-20:38:21.056423 7ff6717ee6c0 Delete type=0 #473
2025/11/21-23:35:34.777225 7f21ed7fa6c0 Level-0 table #445: started 2026/05/13-21:29:49.443482 7ff6637fe6c0 Level-0 table #478: started
2025/11/21-23:35:34.777275 7f21ed7fa6c0 Level-0 table #445: 0 bytes OK 2026/05/13-21:29:49.443606 7ff6637fe6c0 Level-0 table #478: 0 bytes OK
2025/11/21-23:35:34.786357 7f21ed7fa6c0 Delete type=0 #443 2026/05/13-21:29:49.450275 7ff6637fe6c0 Delete type=0 #476
2025/11/21-23:35:34.806576 7f21ed7fa6c0 Manual compaction at level-0 from '!folders!5PrT9QmN1cFPzDFP' @ 72057594037927935 : 1 .. '!items!zvoUByzWSWZ87fxA' @ 0 : 0; will stop at (end) 2026/05/13-21:29:49.450420 7ff6637fe6c0 Manual compaction at level-0 from '!folders!5PrT9QmN1cFPzDFP' @ 72057594037927935 : 1 .. '!items!zvoUByzWSWZ87fxA' @ 0 : 0; will stop at (end)
+7 -7
View File
@@ -1,7 +1,7 @@
2025/11/12-23:48:30.659183 7f47817fa6c0 Recovering log #436 2026/05/13-13:05:01.363329 7ff663fff6c0 Recovering log #469
2025/11/12-23:48:30.670042 7f47817fa6c0 Delete type=3 #434 2026/05/13-13:05:01.408343 7ff663fff6c0 Delete type=3 #467
2025/11/12-23:48:30.670124 7f47817fa6c0 Delete type=0 #436 2026/05/13-13:05:01.408405 7ff663fff6c0 Delete type=0 #469
2025/11/13-13:58:42.417482 7f4780bff6c0 Level-0 table #441: started 2026/05/13-13:57:22.802421 7ff6637fe6c0 Level-0 table #474: started
2025/11/13-13:58:42.417509 7f4780bff6c0 Level-0 table #441: 0 bytes OK 2026/05/13-13:57:22.802455 7ff6637fe6c0 Level-0 table #474: 0 bytes OK
2025/11/13-13:58:42.423396 7f4780bff6c0 Delete type=0 #439 2026/05/13-13:57:22.809135 7ff6637fe6c0 Delete type=0 #472
2025/11/13-13:58:42.434882 7f4780bff6c0 Manual compaction at level-0 from '!folders!5PrT9QmN1cFPzDFP' @ 72057594037927935 : 1 .. '!items!zvoUByzWSWZ87fxA' @ 0 : 0; will stop at (end) 2026/05/13-13:57:22.821691 7ff6637fe6c0 Manual compaction at level-0 from '!folders!5PrT9QmN1cFPzDFP' @ 72057594037927935 : 1 .. '!items!zvoUByzWSWZ87fxA' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
View File
View File
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000088 MANIFEST-000121
+7 -7
View File
@@ -1,7 +1,7 @@
2025/11/21-23:23:35.396467 7f21ef7fe6c0 Recovering log #86 2026/05/13-20:38:21.061357 7ff663fff6c0 Recovering log #119
2025/11/21-23:23:35.502570 7f21ef7fe6c0 Delete type=3 #84 2026/05/13-20:38:21.070703 7ff663fff6c0 Delete type=3 #117
2025/11/21-23:23:35.502631 7f21ef7fe6c0 Delete type=0 #86 2026/05/13-20:38:21.070750 7ff663fff6c0 Delete type=0 #119
2025/11/21-23:35:34.786516 7f21ed7fa6c0 Level-0 table #91: started 2026/05/13-21:29:49.386758 7ff6637fe6c0 Level-0 table #124: started
2025/11/21-23:35:34.786543 7f21ed7fa6c0 Level-0 table #91: 0 bytes OK 2026/05/13-21:29:49.386971 7ff6637fe6c0 Level-0 table #124: 0 bytes OK
2025/11/21-23:35:34.796797 7f21ed7fa6c0 Delete type=0 #89 2026/05/13-21:29:49.393349 7ff6637fe6c0 Delete type=0 #122
2025/11/21-23:35:34.806589 7f21ed7fa6c0 Manual compaction at level-0 from '!folders!0DI3T2jve3nsmsfZ' @ 72057594037927935 : 1 .. '!items!zyxA9DhO36t5OBDv' @ 0 : 0; will stop at (end) 2026/05/13-21:29:49.393700 7ff6637fe6c0 Manual compaction at level-0 from '!folders!0DI3T2jve3nsmsfZ' @ 72057594037927935 : 1 .. '!items!zyxA9DhO36t5OBDv' @ 0 : 0; will stop at (end)
+7 -7
View File
@@ -1,7 +1,7 @@
2025/11/12-23:48:30.674156 7f4781ffb6c0 Recovering log #82 2026/05/13-13:05:01.411955 7ff671fef6c0 Recovering log #115
2025/11/12-23:48:30.683495 7f4781ffb6c0 Delete type=3 #80 2026/05/13-13:05:01.463033 7ff671fef6c0 Delete type=3 #113
2025/11/12-23:48:30.683546 7f4781ffb6c0 Delete type=0 #82 2026/05/13-13:05:01.463084 7ff671fef6c0 Delete type=0 #115
2025/11/13-13:58:42.404378 7f4780bff6c0 Level-0 table #87: started 2026/05/13-13:57:22.809226 7ff6637fe6c0 Level-0 table #120: started
2025/11/13-13:58:42.404437 7f4780bff6c0 Level-0 table #87: 0 bytes OK 2026/05/13-13:57:22.809250 7ff6637fe6c0 Level-0 table #120: 0 bytes OK
2025/11/13-13:58:42.410719 7f4780bff6c0 Delete type=0 #85 2026/05/13-13:57:22.815441 7ff6637fe6c0 Delete type=0 #118
2025/11/13-13:58:42.434856 7f4780bff6c0 Manual compaction at level-0 from '!folders!0DI3T2jve3nsmsfZ' @ 72057594037927935 : 1 .. '!items!zyxA9DhO36t5OBDv' @ 0 : 0; will stop at (end) 2026/05/13-13:57:22.821701 7ff6637fe6c0 Manual compaction at level-0 from '!folders!0DI3T2jve3nsmsfZ' @ 72057594037927935 : 1 .. '!items!zyxA9DhO36t5OBDv' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -33,7 +33,7 @@
}, },
"compatibility": { "compatibility": {
"minimum": "13", "minimum": "13",
"verified": "13" "verified": "14"
}, },
"esmodules": [ "esmodules": [
"cthulhu-eternal.mjs" "cthulhu-eternal.mjs"
+1
View File
@@ -8,6 +8,7 @@
{{formField systemFields.settings value=system.settings localize=true}} {{formField systemFields.settings value=system.settings localize=true}}
{{formField systemFields.protection value=system.protection}} {{formField systemFields.protection value=system.protection}}
{{formField systemFields.resourceLevel value=system.resourceLevel}} {{formField systemFields.resourceLevel value=system.resourceLevel}}
{{formField systemFields.equipped value=system.equipped}}
</fieldset> </fieldset>
+14 -4
View File
@@ -22,10 +22,20 @@
{{/if}} {{/if}}
<li class="li-apply-wounds"> <li class="li-apply-wounds">
<button type="button" class="apply-wounds">{{localize "CTHULHUETERNAL.Label.applyWounds"}}</button> <div>{{localize "CTHULHUETERNAL.Label.applyWounds"}}</div>
<select name="combatant" class="roll-skill-modifier"> <div class="combatants-grid">
{{selectOptions combatants valueAttr="id" labelAttr="name"}} {{#each combatants}}
</select> <button
class="apply-wounds-btn chat-action-button"
data-combatant-id="{{this.id}}"
data-actor-id="{{this.actorId}}"
data-token-id="{{this.tokenId}}"
title="{{this.name}}"
>
{{this.name}}
</button>
{{/each}}
</div>
</li> </li>
{{#if isLethal}} {{#if isLethal}}
+35
View File
@@ -54,6 +54,13 @@
: -20%</li> : -20%</li>
{{/if}} {{/if}}
{{#if isWorn}}
<li class="orange-warning"><i
class="fa-solid fa-triangle-exclamation"
></i>
{{localize "CTHULHUETERNAL.Label.wornWeaponWarning"}}</li>
{{/if}}
{{#if (eq rollType "resource")}} {{#if (eq rollType "resource")}}
<li>{{localize "CTHULHUETERNAL.Label.multiplier"}} <li>{{localize "CTHULHUETERNAL.Label.multiplier"}}
: :
@@ -143,6 +150,13 @@
</div> </div>
{{/unless}} {{/unless}}
{{#if skillMarkedForProgress}}
<p class="skill-progress-notice">
<i class="fa-solid fa-arrow-up-right-dots"></i>
{{rollItem.name}}{{localize "CTHULHUETERNAL.Label.skillFailed"}}
</p>
{{/if}}
{{! Zone d'actions regroupées }} {{! Zone d'actions regroupées }}
<div class="chat-actions"> <div class="chat-actions">
{{#if isSuccess}} {{#if isSuccess}}
@@ -206,6 +220,18 @@
{{/if}} {{/if}}
{{/if}} {{/if}}
{{#if (eq rollType "weapon")}}
{{#if isWorn}}
<a
class="worn-weapon-check chat-action-button"
data-weapon-id="{{weapon.id}}"
data-tooltip="{{localize 'CTHULHUETERNAL.Label.wornWeaponCheck'}}"
>
<i class="fa-solid fa-dice"></i>
</a>
{{/if}}
{{/if}}
{{#if isFailure}} {{#if isFailure}}
{{#if isNudge}} {{#if isNudge}}
<a <a
@@ -214,6 +240,15 @@
> >
<i class="fa-solid fa-circle-sort-down"></i> <i class="fa-solid fa-circle-sort-down"></i>
</a> </a>
{{#if wpCostToSucceed}}
<a
class="nudge-to-success chat-action-button"
data-tooltip="{{localize 'CTHULHUETERNAL.Roll.nudgeToSuccess' score=targetScore wp=wpCostToSucceed}}"
>
<i class="fa-solid fa-circle-check"></i>
{{localize "CTHULHUETERNAL.Roll.nudgeToSuccess" score=targetScore wp=wpCostToSucceed}}
</a>
{{/if}}
{{/if}} {{/if}}
{{/if}} {{/if}}
+3 -1
View File
@@ -39,8 +39,10 @@
<div class="combatants-grid"> <div class="combatants-grid">
{{#each combatants}} {{#each combatants}}
<button <button
class="apply-wounds-btn" class="apply-wounds-btn chat-action-button"
data-combatant-id="{{this.id}}" data-combatant-id="{{this.id}}"
data-actor-id="{{this.actorId}}"
data-token-id="{{this.tokenId}}"
title="{{this.name}}" title="{{this.name}}"
> >
{{this.name}} {{this.name}}
+10 -10
View File
@@ -8,19 +8,19 @@
{{/if}} {{/if}}
<li class="san-loose-buttons"> <li class="san-loose-buttons">
<button class="san-loose" data-san-value="0">0</button> <button class="san-loose chat-action-button" data-san-value="0">0</button>
<button class="san-loose" data-san-value="1">1</button> <button class="san-loose chat-action-button" data-san-value="1">1</button>
<button class="san-loose" data-san-value="2">2</button> <button class="san-loose chat-action-button" data-san-value="2">2</button>
<button class="san-loose" data-san-value="3">3</button> <button class="san-loose chat-action-button" data-san-value="3">3</button>
<button class="san-loose" data-san-value="4">4</button> <button class="san-loose chat-action-button" data-san-value="4">4</button>
</li> </li>
<li class="san-loose-buttons"> <li class="san-loose-buttons">
<button class="san-loose" data-san-value="1d4">1d4</button> <button class="san-loose chat-action-button" data-san-value="1d4">1d4</button>
<button class="san-loose" data-san-value="1d6">1d6</button> <button class="san-loose chat-action-button" data-san-value="1d6">1d6</button>
<button class="san-loose" data-san-value="1d8">1d8</button> <button class="san-loose chat-action-button" data-san-value="1d8">1d8</button>
<button class="san-loose" data-san-value="1d10">1d10</button> <button class="san-loose chat-action-button" data-san-value="1d10">1d10</button>
<button class="san-loose" data-san-value="1d12">1d12</button> <button class="san-loose chat-action-button" data-san-value="1d12">1d12</button>
</li> </li>
</ul> </ul>
+4 -4
View File
@@ -5,12 +5,12 @@
<li><strong>{{localize "CTHULHUETERNAL.Label.selectSANType"}}</strong></li> <li><strong>{{localize "CTHULHUETERNAL.Label.selectSANType"}}</strong></li>
<li class="san-type-buttons"> <li class="san-type-buttons">
<button class="san-type" data-san-value="{{sanLoss}}" data-san-type="violence">{{localize "CTHULHUETERNAL.Label.Violence"}}</button> <button class="san-type chat-action-button" data-san-value="{{sanLoss}}" data-san-type="violence">{{localize "CTHULHUETERNAL.Label.Violence"}}</button>
<button class="san-type" data-san-value="{{sanLoss}}" data-san-type="helplessness">{{localize "CTHULHUETERNAL.Label.Helplessness"}}</button> <button class="san-type chat-action-button" data-san-value="{{sanLoss}}" data-san-type="helplessness">{{localize "CTHULHUETERNAL.Label.Helplessness"}}</button>
</li> </li>
<li class="san-type-buttons"> <li class="san-type-buttons">
<button class="san-type" data-san-value="{{sanLoss}}" data-san-type="unnatural">{{localize "CTHULHUETERNAL.Label.Unnatural"}}</button> <button class="san-type chat-action-button" data-san-value="{{sanLoss}}" data-san-type="unnatural">{{localize "CTHULHUETERNAL.Label.Unnatural"}}</button>
<button class="san-type" data-san-value="{{sanLoss}}" data-san-type="none">{{localize "CTHULHUETERNAL.Label.None"}}</button> <button class="san-type chat-action-button" data-san-value="{{sanLoss}}" data-san-type="none">{{localize "CTHULHUETERNAL.Label.None"}}</button>
</li> </li>
</ul> </ul>
+1 -1
View File
@@ -6,7 +6,7 @@
<fieldset> <fieldset>
<legend>{{localize "CTHULHUETERNAL.Label.description"}}</legend> <legend>{{localize "CTHULHUETERNAL.Label.description"}}</legend>
{{formInput systemFields.description enriched=enrichedDescriptionw value=system.description name="system.description" toggled=true}} {{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}}
</fieldset> </fieldset>
</section> </section>
+14 -1
View File
@@ -9,7 +9,7 @@
{{#each weapons as |item|}} {{#each weapons as |item|}}
{{!log 'weapon' this}} {{!log 'weapon' this}}
<div class="weapon item" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-drag="true"> <div class="weapon item" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-drag="true">
<!-- content -->
<img class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" /> <img class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" />
<img src="systems/fvtt-cthulhu-eternal/assets/ui/d100.svg" class="d100" /> <img src="systems/fvtt-cthulhu-eternal/assets/ui/d100.svg" class="d100" />
<div class="name rollable" data-roll-type="weapon" data-tooltip="{{{item.system.description}}}"> <div class="name rollable" data-roll-type="weapon" data-tooltip="{{{item.system.description}}}">
@@ -81,6 +81,8 @@
data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a> data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>
</div> </div>
</div> </div>
{{else}}
<div class="empty-state">{{localize "CTHULHUETERNAL.Label.noWeapons"}}</div>
{{/each}} {{/each}}
</div> </div>
</fieldset> </fieldset>
@@ -99,6 +101,13 @@
{{item.name}} {{item.name}}
</div> </div>
<span class="protection">{{localize "CTHULHUETERNAL.Label.armor"}} : {{item.system.protection}}</span> <span class="protection">{{localize "CTHULHUETERNAL.Label.armor"}} : {{item.system.protection}}</span>
<a class="equipped-toggle {{#if item.system.equipped}}active{{/if}}"
data-action="toggleEquipped"
data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"
data-tooltip="{{#if item.system.equipped}}{{localize 'CTHULHUETERNAL.Label.equipped'}}{{else}}{{localize 'CTHULHUETERNAL.Label.unequipped'}}{{/if}}">
<i class="fas {{#if item.system.equipped}}fa-shield-halved{{else}}fa-shield{{/if}}"></i>
</a>
<div class="controls"> <div class="controls">
<a data-tooltip="{{localize 'CTHULHUETERNAL.Edit'}}" data-action="edit" data-item-id="{{item.id}}" <a data-tooltip="{{localize 'CTHULHUETERNAL.Edit'}}" data-action="edit" data-item-id="{{item.id}}"
data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a> data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
@@ -106,6 +115,8 @@
data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a> data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>
</div> </div>
</div> </div>
{{else}}
<div class="empty-state">{{localize "CTHULHUETERNAL.Label.noArmors"}}</div>
{{/each}} {{/each}}
</div> </div>
</fieldset> </fieldset>
@@ -130,6 +141,8 @@
data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a> data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>
</div> </div>
</div> </div>
{{else}}
<div class="empty-state">{{localize "CTHULHUETERNAL.Label.noGears"}}</div>
{{/each}} {{/each}}
</div> </div>
</fieldset> </fieldset>
+2
View File
@@ -17,6 +17,8 @@
<a data-tooltip="{{localize 'CTHULHUETERNAL.Delete'}}" data-action="delete" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a> <a data-tooltip="{{localize 'CTHULHUETERNAL.Delete'}}" data-action="delete" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>
</div> </div>
</div> </div>
{{else}}
<div class="empty-state">{{localize "CTHULHUETERNAL.Label.noSkills"}}</div>
{{/each}} {{/each}}
</div> </div>
</fieldset> </fieldset>
+10
View File
@@ -44,6 +44,16 @@
{{/if}} {{/if}}
{{#if weapon}} {{#if weapon}}
{{#if isJunk}}
<div class="dialog-skill red-warning">{{localize
"CTHULHUETERNAL.Label.JunkWeapon"
}}</div>
{{/if}}
{{#if isWorn}}
<div class="dialog-skill orange-warning">{{localize
"CTHULHUETERNAL.Label.WornWeapon"
}}</div>
{{/if}}
<div class="dialog-skill">{{localize "CTHULHUETERNAL.Label.Weapon"}} <div class="dialog-skill">{{localize "CTHULHUETERNAL.Label.Weapon"}}
: :
{{weapon.name}}</div> {{weapon.name}}</div>
+46
View File
@@ -0,0 +1,46 @@
import os
def check_utf8_encoding(filenames):
"""
Checks a list of filenames to detect potential UTF-8 encoding errors.
Args:
filenames (list): A list of strings representing filenames to check.
Returns:
dict: A dictionary where keys are filenames with errors and values are the error messages.
"""
error_report = {}
print("--- Starting UTF-8 Encoding Check ---")
for filename in filenames:
try:
# Attempt to decode the filename as UTF-8
filename.encode('utf-8')
# If encode succeeds, it's likely valid UTF-8
print(f"SUCCESS: {filename}")
except UnicodeEncodeError as e:
# If encoding fails, it indicates invalid UTF-8 sequences
error_report[filename] = str(e)
print(f"ERROR: {filename} - Decoding failed: {e}")
print("--- Check Complete ---")
if error_report:
print("\n--- Summary of Errors ---")
for filename, error in error_report.items():
print(f"File: {filename}\n Error: {error}\n")
else:
print("\nAll checked filenames appear to be valid UTF-8.")
return error_report
if __name__ == "__main__":
# Example list of filenames to check. Replace these with your actual directory contents.
files_to_check = [
"valid_file.txt",
"file_with_bad_byte\x80.bin", # Example of a potentially bad filename
"another_valid_one.md",
"file_with_another_error\x99.dat"
]
check_utf8_encoding(files_to_check)