fixed die
This commit is contained in:
@@ -27,6 +27,14 @@ const info = {
|
||||
type: ParameterType.STRING,
|
||||
default: 'You rolled a {{value}}.',
|
||||
},
|
||||
practice_trial: {
|
||||
type: ParameterType.BOOL,
|
||||
default: false,
|
||||
},
|
||||
practice_roll_limit: {
|
||||
type: ParameterType.INT,
|
||||
default: 5,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -37,13 +45,17 @@ class DieRollPlugin {
|
||||
|
||||
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];
|
||||
const practice_enabled = Boolean(trial.practice_trial);
|
||||
const practice_roll_limit = practice_enabled
|
||||
? this.get_practice_roll_limit(trial.practice_roll_limit)
|
||||
: 1;
|
||||
const sanitized_rigged_value = practice_enabled
|
||||
? null
|
||||
: this.get_rigged_value(trial.rigged_value);
|
||||
const get_next_result_value = () =>
|
||||
sanitized_rigged_value ?? this.get_random_value();
|
||||
let pending_final_value = get_next_result_value();
|
||||
let last_roll_value = null;
|
||||
|
||||
display_element.innerHTML = this.build_markup(
|
||||
trial.prompt,
|
||||
@@ -60,21 +72,60 @@ class DieRollPlugin {
|
||||
'.jspsych_die_roll_button'
|
||||
);
|
||||
|
||||
const PRACTICE_UNLOCK_DELAY_MS = 600;
|
||||
const MIN_IDLE_CLICK_INTERVAL_MS = 350;
|
||||
let is_rolling = false;
|
||||
let has_rolled = false;
|
||||
let roll_started_at = null;
|
||||
let roll_stopped_at = null;
|
||||
let die_click_count = 0;
|
||||
let practice_rolls_completed = 0;
|
||||
let practice_reenable_timeout = null;
|
||||
let previous_face_value = null;
|
||||
let last_face_interaction_at = null;
|
||||
|
||||
const clear_timeouts = () => {
|
||||
if (practice_enabled && practice_reenable_timeout !== null) {
|
||||
window.clearTimeout(practice_reenable_timeout);
|
||||
practice_reenable_timeout = null;
|
||||
}
|
||||
this.jsPsych.pluginAPI.clearAllTimeouts();
|
||||
};
|
||||
|
||||
const lock_die_face = () => {
|
||||
face_element.classList.remove('jspsych_die_roll_face--interactive');
|
||||
face_element.classList.remove('jspsych_die_roll_face--active');
|
||||
face_element.classList.add('jspsych_die_roll_face--locked');
|
||||
face_element.setAttribute('aria-disabled', 'true');
|
||||
face_element.removeAttribute('tabindex');
|
||||
face_element.style.pointerEvents = 'none';
|
||||
};
|
||||
|
||||
const make_die_interactive = () => {
|
||||
face_element.classList.remove('jspsych_die_roll_face--locked');
|
||||
face_element.classList.add('jspsych_die_roll_face--interactive');
|
||||
face_element.setAttribute('tabindex', '0');
|
||||
face_element.removeAttribute('aria-disabled');
|
||||
face_element.style.pointerEvents = '';
|
||||
};
|
||||
|
||||
const get_next_face_value = () => {
|
||||
let value;
|
||||
do {
|
||||
value = Math.floor(Math.random() * 6) + 1;
|
||||
} while (
|
||||
value === previous_face_value ||
|
||||
(sanitized_rigged_value !== null && value === sanitized_rigged_value)
|
||||
);
|
||||
previous_face_value = value;
|
||||
return value;
|
||||
};
|
||||
|
||||
const cycle_face = () => {
|
||||
if (!is_rolling) {
|
||||
return;
|
||||
}
|
||||
const value = this.random_face(final_value);
|
||||
const value = get_next_face_value();
|
||||
face_element.textContent = value;
|
||||
this.jsPsych.pluginAPI.setTimeout(cycle_face, trial.animation_interval);
|
||||
};
|
||||
@@ -88,25 +139,55 @@ class DieRollPlugin {
|
||||
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
|
||||
last_roll_value = pending_final_value;
|
||||
face_element.textContent = last_roll_value;
|
||||
lock_die_face();
|
||||
practice_rolls_completed += 1;
|
||||
const rolls_remaining = Math.max(
|
||||
practice_roll_limit - practice_rolls_completed,
|
||||
0
|
||||
);
|
||||
const formatted_result = this.render_result_text(
|
||||
trial.result_template,
|
||||
{
|
||||
value: last_roll_value,
|
||||
roll_number: practice_rolls_completed,
|
||||
roll_limit: practice_enabled ? practice_roll_limit : null,
|
||||
rolls_remaining: practice_enabled ? rolls_remaining : null,
|
||||
}
|
||||
);
|
||||
|
||||
if (practice_enabled) {
|
||||
if (rolls_remaining > 0) {
|
||||
result_element.innerHTML =
|
||||
`${formatted_result} <p class = "mt-2"> You may roll again (${rolls_remaining} ` +
|
||||
`practice ${rolls_remaining === 1 ? 'roll' : 'rolls'} remaining) or click 'Continue' to proceed.</p>`;
|
||||
// Brief delay prevents a double-click from immediately starting a new roll.
|
||||
practice_reenable_timeout = window.setTimeout(() => {
|
||||
make_die_interactive();
|
||||
has_rolled = false;
|
||||
practice_reenable_timeout = null;
|
||||
}, PRACTICE_UNLOCK_DELAY_MS);
|
||||
} else {
|
||||
result_element.textContent =
|
||||
`${formatted_result} Practice complete. Click 'Continue' to proceed.`;
|
||||
}
|
||||
} else {
|
||||
result_element.textContent = formatted_result;
|
||||
}
|
||||
|
||||
button_element.disabled = false;
|
||||
button_element.focus();
|
||||
if (!practice_enabled || rolls_remaining === 0) {
|
||||
button_element.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const begin_roll = () => {
|
||||
if (is_rolling || has_rolled) {
|
||||
return;
|
||||
}
|
||||
previous_face_value = null;
|
||||
pending_final_value = get_next_result_value();
|
||||
is_rolling = true;
|
||||
die_click_count += 1;
|
||||
roll_started_at = performance.now();
|
||||
@@ -117,31 +198,59 @@ class DieRollPlugin {
|
||||
cycle_face();
|
||||
};
|
||||
|
||||
const handle_face_interaction = () => {
|
||||
if (!is_rolling && !has_rolled) {
|
||||
begin_roll();
|
||||
} else if (is_rolling && !has_rolled) {
|
||||
const handle_face_interaction = event => {
|
||||
const event_type = event?.type ?? 'unknown';
|
||||
|
||||
if (
|
||||
(event_type === 'pointerdown' || event_type === 'click') &&
|
||||
typeof event?.button === 'number' &&
|
||||
event.button !== 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (practice_enabled) {
|
||||
const now = performance.now();
|
||||
if (!is_rolling && last_face_interaction_at !== null) {
|
||||
const elapsed = now - last_face_interaction_at;
|
||||
if (elapsed < MIN_IDLE_CLICK_INTERVAL_MS) {
|
||||
event?.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
last_face_interaction_at = now;
|
||||
}
|
||||
|
||||
if (is_rolling) {
|
||||
finalize_roll();
|
||||
return;
|
||||
}
|
||||
if (!has_rolled) {
|
||||
begin_roll();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
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');
|
||||
result_element.innerHTML = practice_enabled
|
||||
? `<p>Practice rolling the die. You can complete up to ${practice_roll_limit}</p>` +
|
||||
`practice ${practice_roll_limit === 1 ? 'roll' : 'rolls'}. <p class = "mt-2"> Click the die to start, then click it again to stop. </p>`
|
||||
: 'Click the die to start, then click it again to lock in your number.';
|
||||
make_die_interactive();
|
||||
face_element.setAttribute('role', 'button');
|
||||
face_element.setAttribute(
|
||||
'aria-label',
|
||||
'Virtual die. Click to start and stop.'
|
||||
);
|
||||
|
||||
face_element.addEventListener('click', handle_face_interaction);
|
||||
const face_interaction_event =
|
||||
typeof window.PointerEvent === 'function' ? 'pointerdown' : 'click';
|
||||
face_element.addEventListener(face_interaction_event, handle_face_interaction);
|
||||
face_element.addEventListener('keydown', event => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
handle_face_interaction();
|
||||
handle_face_interaction(event);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -149,7 +258,7 @@ class DieRollPlugin {
|
||||
is_rolling = false;
|
||||
clear_timeouts();
|
||||
const trial_data = {
|
||||
roll: final_value,
|
||||
roll: last_roll_value,
|
||||
rigged_value: sanitized_rigged_value,
|
||||
rigged: sanitized_rigged_value !== null,
|
||||
die_click_count: die_click_count,
|
||||
@@ -161,6 +270,14 @@ class DieRollPlugin {
|
||||
? roll_stopped_at - roll_started_at
|
||||
: null,
|
||||
rt: performance.now() - start_time,
|
||||
practice_trial: practice_enabled,
|
||||
practice_roll_limit: practice_enabled ? practice_roll_limit : null,
|
||||
practice_rolls_completed: practice_enabled
|
||||
? practice_rolls_completed
|
||||
: null,
|
||||
practice_rolls_remaining: practice_enabled
|
||||
? Math.max(practice_roll_limit - practice_rolls_completed, 0)
|
||||
: null,
|
||||
};
|
||||
display_element.innerHTML = '';
|
||||
this.jsPsych.finishTrial(trial_data);
|
||||
@@ -187,12 +304,11 @@ class DieRollPlugin {
|
||||
`;
|
||||
}
|
||||
|
||||
random_face(exclude_value) {
|
||||
let value = Math.floor(Math.random() * 6) + 1;
|
||||
if (value === exclude_value) {
|
||||
value = (value % 6) + 1;
|
||||
}
|
||||
return value;
|
||||
get_random_value() {
|
||||
return this.jsPsych.randomization.sampleWithoutReplacement(
|
||||
[1, 2, 3, 4, 5, 6],
|
||||
1
|
||||
)[0];
|
||||
}
|
||||
|
||||
get_rigged_value(value) {
|
||||
@@ -202,6 +318,27 @@ class DieRollPlugin {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get_practice_roll_limit(value) {
|
||||
const numeric = Number(value);
|
||||
if (Number.isFinite(numeric) && numeric >= 1) {
|
||||
return Math.round(numeric);
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
render_result_text(template, replacements) {
|
||||
if (typeof template !== 'string' || template.length === 0) {
|
||||
return '';
|
||||
}
|
||||
return Object.entries(replacements).reduce((text, [key, value]) => {
|
||||
if (value === null || value === undefined) {
|
||||
return text;
|
||||
}
|
||||
const pattern = new RegExp(`\\{\\{${key}\\}\\}`, 'gi');
|
||||
return text.replace(pattern, value);
|
||||
}, template);
|
||||
}
|
||||
}
|
||||
|
||||
DieRollPlugin.info = info;
|
||||
|
||||
Reference in New Issue
Block a user