import { ParameterType } from 'jspsych'; const info = { name: 'die-roll', parameters: { prompt: { type: ParameterType.HTML_STRING, default: '', }, rigged_value: { type: ParameterType.INT, default: null, }, roll_duration: { type: ParameterType.INT, default: 2000, }, animation_interval: { type: ParameterType.INT, default: 120, }, button_label: { type: ParameterType.STRING, default: 'Continue', }, result_template: { type: ParameterType.STRING, default: 'You rolled a {{value}}.', }, }, }; class DieRollPlugin { constructor(jsPsych) { this.jsPsych = jsPsych; } trial(display_element, trial) { const start_time = performance.now(); const sanitized_rigged_value = this.get_rigged_value(trial.rigged_value); const final_value = sanitized_rigged_value ?? this.jsPsych.randomization.sampleWithoutReplacement( [1, 2, 3, 4, 5, 6], 1 )[0]; display_element.innerHTML = this.build_markup( trial.prompt, trial.button_label ); const face_element = display_element.querySelector( '.jspsych_die_roll_face' ); const result_element = display_element.querySelector( '.jspsych_die_roll_result' ); const button_element = display_element.querySelector( '.jspsych_die_roll_button' ); let is_rolling = false; let has_rolled = false; let roll_started_at = null; let roll_stopped_at = null; let die_click_count = 0; const clear_timeouts = () => { this.jsPsych.pluginAPI.clearAllTimeouts(); }; const cycle_face = () => { if (!is_rolling) { return; } const value = this.random_face(final_value); face_element.textContent = value; this.jsPsych.pluginAPI.setTimeout(cycle_face, trial.animation_interval); }; const finalize_roll = () => { if (!is_rolling || has_rolled) { return; } is_rolling = false; has_rolled = true; die_click_count += 1; roll_stopped_at = performance.now(); clear_timeouts(); face_element.textContent = final_value; face_element.classList.remove('jspsych_die_roll_face--active'); face_element.classList.remove('jspsych_die_roll_face--interactive'); face_element.classList.add('jspsych_die_roll_face--locked'); face_element.setAttribute('aria-disabled', 'true'); face_element.removeAttribute('tabindex'); face_element.style.pointerEvents = 'none'; result_element.textContent = trial.result_template.replace( /\{\{value\}\}/gi, final_value ); button_element.disabled = false; button_element.focus(); }; const begin_roll = () => { if (is_rolling || has_rolled) { return; } is_rolling = true; die_click_count += 1; roll_started_at = performance.now(); result_element.textContent = 'Rolling... click again to stop the die.'; face_element.classList.add('jspsych_die_roll_face--active'); button_element.disabled = true; clear_timeouts(); cycle_face(); }; const handle_face_interaction = () => { if (!is_rolling && !has_rolled) { begin_roll(); } else if (is_rolling && !has_rolled) { finalize_roll(); } }; button_element.disabled = true; face_element.textContent = ''; result_element.textContent = 'Click the die to start, then click it again to lock in your number.'; face_element.classList.add('jspsych_die_roll_face--interactive'); face_element.setAttribute('tabindex', '0'); face_element.setAttribute('role', 'button'); face_element.setAttribute( 'aria-label', 'Virtual die. Click to start and stop.' ); face_element.addEventListener('click', handle_face_interaction); face_element.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); handle_face_interaction(); } }); const end_trial = () => { is_rolling = false; clear_timeouts(); const trial_data = { roll: final_value, rigged_value: sanitized_rigged_value, rigged: sanitized_rigged_value !== null, die_click_count: die_click_count, roll_started_after_ms: roll_started_at ? roll_started_at - start_time : null, roll_duration_ms: roll_started_at && roll_stopped_at ? roll_stopped_at - roll_started_at : null, rt: performance.now() - start_time, }; display_element.innerHTML = ''; this.jsPsych.finishTrial(trial_data); }; button_element.addEventListener('click', () => { if (!button_element.disabled) { end_trial(); } }); } build_markup(prompt, button_label) { const prompt_html = prompt ? `
${prompt}
` : ''; return `
${prompt_html}
?
`; } random_face(exclude_value) { let value = Math.floor(Math.random() * 6) + 1; if (value === exclude_value) { value = (value % 6) + 1; } return value; } get_rigged_value(value) { const numeric = Number(value); if (Number.isFinite(numeric) && numeric >= 1 && numeric <= 6) { return Math.round(numeric); } return null; } } DieRollPlugin.info = info; export default DieRollPlugin;