240 lines
8.4 KiB
JavaScript
240 lines
8.4 KiB
JavaScript
Hooks.once('init', () => {
|
|
// Register Settings
|
|
game.settings.register('dh-immersive-crits', 'critAnimation', {
|
|
name: 'Critical Hit Animation',
|
|
hint: 'Select the visual effect for critical hit chat cards.',
|
|
scope: 'world',
|
|
config: true,
|
|
type: String,
|
|
choices: {
|
|
"none": "None",
|
|
"embers": "Embers",
|
|
"pulse": "Pulse",
|
|
"arcane": "Arcane",
|
|
"holy": "Holy",
|
|
"necrotic": "Necrotic",
|
|
"nature": "Nature"
|
|
},
|
|
default: 'embers'
|
|
});
|
|
|
|
game.settings.register('dh-immersive-crits', 'pulseColor', {
|
|
name: 'Pulse Color',
|
|
hint: 'Select the color for the Pulse animation.',
|
|
scope: 'world',
|
|
config: true,
|
|
type: String,
|
|
default: '#ff0000'
|
|
});
|
|
|
|
game.settings.register('dh-immersive-crits', 'dsnCritEffect', {
|
|
name: 'DiceSoNice Critical Effect',
|
|
hint: 'Select the DiceSoNice special effect to play on critical hits.',
|
|
scope: 'world',
|
|
config: true,
|
|
type: String,
|
|
choices: {
|
|
"none": "None",
|
|
"PlayAnimationBright": "White Glow",
|
|
"PlayAnimationDark": "Darkness",
|
|
"PlayAnimationImpact": "Glass Impact",
|
|
"PlayAnimationParticleSpiral": "Double Spirals",
|
|
"PlayAnimationParticleSparkles": "Blaze",
|
|
"PlayAnimationParticleVortex": "Magic Vortex"
|
|
},
|
|
default: 'PlayAnimationParticleSparkles'
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// Hook to handle conditional settings in the UI
|
|
Hooks.on('renderSettingsConfig', (app, html, data) => {
|
|
// Search for our specific inputs with safer selectors (ending with key)
|
|
const $html = $(html);
|
|
const animSelect = $html.find('select[name$="critAnimation"]');
|
|
const colorInput = $html.find('input[name$="pulseColor"]');
|
|
|
|
// Ensure we found the color input to transform it
|
|
if (colorInput.length) {
|
|
// Enforce Color Picker Type using prop for property change
|
|
colorInput.prop('type', 'color');
|
|
|
|
// Ensure standard height for the picker
|
|
colorInput.css({
|
|
'height': '28px',
|
|
'width': '50px',
|
|
'padding': '0',
|
|
'border': 'none',
|
|
'cursor': 'pointer',
|
|
'background': 'none'
|
|
});
|
|
|
|
// If the value is somehow empty, set a default
|
|
if (!colorInput.val()) colorInput.val('#ff0000');
|
|
}
|
|
|
|
// Handle Conditional Visibility
|
|
if (animSelect.length && colorInput.length) {
|
|
// Try to find the container. .form-group is standard, but sometimes it's direct parent
|
|
const colorGroup = colorInput.closest('.form-group');
|
|
|
|
const toggleColor = () => {
|
|
const val = animSelect.val();
|
|
// Show only if 'pulse' is selected
|
|
if (val === 'pulse') {
|
|
colorGroup.show();
|
|
} else {
|
|
colorGroup.hide();
|
|
}
|
|
};
|
|
|
|
// Initial check
|
|
toggleColor();
|
|
|
|
// Bind change event
|
|
animSelect.on('change', toggleColor);
|
|
}
|
|
});
|
|
|
|
// Helper to determine if a roll is a critical hit based on module rules
|
|
function isCriticalHit(roll) {
|
|
if (!roll) return false;
|
|
|
|
// Check for Duality Roll (Class or Structure)
|
|
// DSN might pass a BaseRoll, so we check properties or terms
|
|
const isDuality = roll.constructor.name === "DualityRoll" ||
|
|
(roll.dHope && roll.dFear) ||
|
|
(roll.terms && roll.terms.filter(t => t.faces === 12).length === 2);
|
|
|
|
if (isDuality) {
|
|
// Exclude reaction rolls
|
|
const isReaction = roll.options?.actionType === "reaction";
|
|
if (isReaction) return false;
|
|
|
|
let hopeTotal, fearTotal;
|
|
|
|
if (roll.dHope?.total !== undefined && roll.dFear?.total !== undefined) {
|
|
hopeTotal = roll.dHope.total;
|
|
fearTotal = roll.dFear.total;
|
|
} else {
|
|
// Fallback: extract from terms
|
|
const d12s = roll.terms.filter(t => t.faces === 12);
|
|
if (d12s.length >= 2) {
|
|
// Assuming standard order or just checking equality
|
|
// In Duality, Hope and Fear are the two d12s. Equality is what matters.
|
|
hopeTotal = d12s[0].total;
|
|
fearTotal = d12s[1].total;
|
|
}
|
|
}
|
|
|
|
if (hopeTotal !== undefined && fearTotal !== undefined) {
|
|
return hopeTotal === fearTotal;
|
|
}
|
|
}
|
|
|
|
// Check for standard d20 Roll (GM rolls mainly)
|
|
if (roll.constructor.name === "D20Roll") {
|
|
if (roll.d20?.total === 20) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Hook into renderChatMessageHTML (replacement for renderChatMessage)
|
|
Hooks.on('renderChatMessageHTML', (message, html, data) => {
|
|
// Only proceed if we have a roll
|
|
if (!message.rolls || message.rolls.length === 0) return;
|
|
|
|
const roll = message.rolls[0];
|
|
if (isCriticalHit(roll)) {
|
|
const anim = game.settings.get('dh-immersive-crits', 'critAnimation');
|
|
if (anim && anim !== 'none') {
|
|
const $html = $(html);
|
|
const content = $html.find('.message-content');
|
|
content.addClass(`dh-crit-${anim}`);
|
|
|
|
// Apply custom color for Pulse
|
|
if (anim === 'pulse') {
|
|
const pulseColor = game.settings.get('dh-immersive-crits', 'pulseColor');
|
|
content.css('--dh-crit-pulse-color', pulseColor);
|
|
}
|
|
|
|
// Apply random scale for Holy
|
|
if (anim === 'holy') {
|
|
const scale = 0.8 + Math.random() * 0.5; // 0.8 - 1.3 scale
|
|
content.css('--dh-holy-scale', scale);
|
|
}
|
|
|
|
// Inject particles for Embers to allow individual randomization
|
|
if (anim === 'embers') {
|
|
const particleContainer = $('<div class="dh-particle-container"></div>');
|
|
// Create 12 particles
|
|
for (let i = 0; i < 12; i++) {
|
|
const particle = $('<div class="dh-ember-particle"></div>');
|
|
// Randomize properties via CSS variables
|
|
const duration = 2 + Math.random() * 3; // 2-5s
|
|
const delay = Math.random() * 2; // 0-2s delay
|
|
const left = Math.random() * 100; // 0-100% width
|
|
const size = 3 + Math.random() * 4; // 3-7px
|
|
const drift = 20 + Math.random() * 60; // 20-80px right drift (more variance)
|
|
const opacity = 0.85 + Math.random() * 0.15; // 0.85 - 1.0 peak opacity
|
|
|
|
particle.css({
|
|
'--dh-anim-duration': `${duration}s`,
|
|
'--dh-anim-delay': `${delay}s`,
|
|
'--dh-left': `${left}%`,
|
|
'--dh-size': `${size}px`,
|
|
'--dh-drift': `${drift}px`,
|
|
'--dh-opacity-peak': opacity
|
|
});
|
|
particleContainer.append(particle);
|
|
}
|
|
content.append(particleContainer);
|
|
}
|
|
|
|
// Also add a general class for easier targeting if needed
|
|
$html.addClass('dh-critical-hit');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Hook into DiceSoNice to trigger effects
|
|
Hooks.on('diceSoNiceRollStart', (messageId, context) => {
|
|
const roll = context.roll;
|
|
if (!roll) return;
|
|
|
|
if (isCriticalHit(roll)) {
|
|
const effect = game.settings.get('dh-immersive-crits', 'dsnCritEffect');
|
|
|
|
if (effect && effect !== 'none') {
|
|
// Function to apply effect to a die/term
|
|
const applyEffect = (term) => {
|
|
if (!term.options) term.options = {};
|
|
// 'effect' key from settings is now the direct Class Name (PascalCase)
|
|
term.options.sfx = {
|
|
specialEffect: effect
|
|
};
|
|
};
|
|
|
|
// Check if roll.dice exists and has elements
|
|
if (roll.dice && roll.dice.length > 0) {
|
|
roll.dice.forEach(die => applyEffect(die));
|
|
} else {
|
|
// Determine if we need to iterate terms for Duality Rolls
|
|
if (roll.terms) {
|
|
roll.terms.forEach(term => {
|
|
if (term.faces) { // It's a die
|
|
applyEffect(term);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
function capitalize(s) {
|
|
if (typeof s !== 'string') return '';
|
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
}
|