Files
suspicion-checks-experiment-2/scripts/plugin-object-moving.js
2025-07-02 00:10:11 +02:00

302 lines
10 KiB
JavaScript

import { ParameterType } from 'jspsych';
import { html } from './text-stimuli';
import getColourMap from './colours';
const info = {
name: 'object-moving',
version: "1.1",
parameters: {
selector: {
type: ParameterType.STRING,
pretty_name: 'Selector',
},
together_side: {
type: ParameterType.STRING,
pretty_name: 'together side',
},
location: {
type: ParameterType.INT,
pretty_name: 'Location',
},
together_colour: {
type: ParameterType.STRING,
pretty_name: 'Together colour',
},
},
// prettier-ignore
citations: '__CITATIONS__'
};
class jsPsychObjectMoving {
constructor(jsPsych) {
this.jsPsych = jsPsych;
}
static {
this.info = info;
}
trial(display_element, trial) {
const colourMap = getColourMap(trial.together_colour);
const left_goal = trial.together_side === 'left' ? 'together' : 'alone';
const right_goal = trial.together_side === 'left' ? 'alone' : 'together';
const left_colour = trial.together_side === 'left' ? colourMap.get('togetherbg') : colourMap.get('alonebg');
const right_colour = trial.together_side === 'left' ? colourMap.get('alonebg') : colourMap.get('togetherbg');
display_element.innerHTML = html`
<div class="flex flex-row min-w-screen absolute top-0 left-0 min-h-screen">
<div id="${left_goal}-goal" class="w-1/12 ${left_colour}"></div>
<div id="workspace" class="w-10/12"></div>
<div id="${right_goal}-goal" class="w-1/12 ${right_colour}"></div>
<div id="info-box" class="z-10 absolute bottom-1/40 min-w-screen justify-center items-center"></div>
</div>
`;
const workspace = document.getElementById('workspace');
const info_box = document.getElementById('info-box');
const workspace_width = workspace.clientWidth;
const viewport_width = window.visualViewport.width;
const goal_offset = viewport_width * (1/12);
const box_width = viewport_width * (3/100);
const workspace_offset = workspace_width * trial.location/100;
const offset_left = workspace_offset + goal_offset - box_width/2;
const viewport_height = window.visualViewport.height;
const top_offset = viewport_height * .5;
const separation = box_width * 1.5;
const objects = html`
<div id="object-partner" class="object-moving-box ${colourMap.get('object-disabled')} absolute z-10" style="left: ${offset_left}px; top: ${top_offset - separation}px"></div>
<div id="object-participant" class="object-moving-box ${colourMap.get('object-disabled')} absolute z-10" style="left: ${offset_left}px; top: ${top_offset + separation}px"></div>
`;
workspace.innerHTML = objects;
const object_partner = document.getElementById('object-partner');
const object_participant = document.getElementById('object-participant');
object_partner.addEventListener('click', () => issue_alert('partner_click'));
object_participant.addEventListener('click', () => issue_alert('early_click_choice'));
const partner_turn = html`<span class="font-semi-bold text-gray-800 text-xl">Awaiting your partner's selection</span>
<div class='loading'></div>`
let choice;
let go_clicked = false;
let partner_finished = false;
let participant_finished = false;
let partner_choice_made = false;
const jsPsych = this.jsPsych;
let start_time = 0;
let response_time = 0;
if (trial.selector === 'participant') {
display_choices();
} else {
info_box.innerHTML = partner_turn;
const partner_choice_delay = jsPsych.randomization.sampleExGaussian(2000, 250, 1/100, true);
let random = Math.random();
//These probabilities are based on Azaad and Sebanz (2025)
if (trial.location > 48 && trial.location < 52) {
choice = random < 0.64 ? 'together' : 'alone';
} else if (trial.location > 31 && trial.location < 35) {
choice = random < 0.58 ? 'together' : 'alone';
} else if (trial.location > 64 && trial.location < 68) {
choice = random < 0.67 ? 'together' : 'alone';
}
setTimeout(() => {
info_box.innerHTML = '';
clear_boxes();
display_go();
initiate_partner_action();
}, partner_choice_delay);
}
function display_choices(){
info_box.innerHTML = html`<div class="flex flex-row justify-center items-center gap-2">
<div id="${left_goal}-button" class="w-1/12 py-1 text-slate-50 ${left_colour}">${left_goal.charAt(0).toUpperCase() + left_goal.slice(1)}</div>
<div id="${right_goal}-button" class="w-1/12 py-1 text-slate-50 ${right_colour}">${right_goal.charAt(0).toUpperCase() + right_goal.slice(1)}</div>
</div>`;
const left_goal_button = document.getElementById(`${left_goal}-button`);
const right_goal_button = document.getElementById(`${right_goal}-button`);
start_time = Date.now();
left_goal_button.addEventListener('click', () => {
choice = left_goal;
handle_choice_made(left_goal_button,right_goal_button);
});
right_goal_button.addEventListener('click', () => {
choice = right_goal;
handle_choice_made(left_goal_button,right_goal_button);
});
}
function handle_choice_made(left_goal_button,right_goal_button){
left_goal_button.removeEventListener('click', handle_choice_made);
right_goal_button.removeEventListener('click', handle_choice_made);
left_goal_button.remove();
right_goal_button.remove();
clear_boxes();
display_go();
response_time = Date.now() - start_time;
if (choice === 'together') {
initiate_partner_action();
}
}
function initiate_partner_action(){
const go_delay = trial.selector === 'participant' ? 1400 : 1000
const partner_go_delay = jsPsych.randomization.sampleExGaussian(go_delay, 500, 1/100, true);
const partner_teleport_delay = jsPsych.randomization.sampleExGaussian(1400, 300, 1/100, true);
setTimeout(() => {
change_object_colour(object_partner);
partner_choice_made = true;
}, partner_go_delay);
setTimeout(() => {
teleport_object(object_partner);
partner_finished = true;
check_finished();
}, partner_teleport_delay + partner_go_delay);
}
function clear_boxes() {
if (choice === 'together') {
return;
}
if (trial.selector === 'participant') {
object_partner.remove();
} else {
object_participant.remove();
}
}
let go_button;
function display_go() {
if (choice === 'alone' && trial.selector === 'partner') {
return;
}
info_box.innerHTML = html`<div id="go-button" class="mx-auto font-semi-bold ${colourMap.get('gobg')} text-slate-50 text-xl w-1/20 p-2">Go</div>`;
go_button = document.getElementById('go-button');
object_participant.addEventListener('click', () => issue_alert('early_click_go'));
go_button.addEventListener('click', () => handle_go_click(go_button));
}
function handle_go_click() {
go_button.removeEventListener('click', handle_go_click);
go_button.removeEventListener('click', () => issue_alert('early_click_go'));
go_clicked = true;
go_button.remove();
change_object_colour(object_participant);
object_participant.addEventListener('click', () => handle_teleport_click());
}
function handle_teleport_click(){
teleport_object(object_participant);
participant_finished = true;
check_finished();
}
function change_object_colour(object){
object.classList.remove(colourMap.get('object-disabled'));
object.classList.add(colourMap.get('object-enabled'));
}
function teleport_object(object){
let goal_side = 'left';
if (choice === 'together' && trial.together_side === 'right') {
goal_side = 'right';
}
if (choice === 'alone' && trial.together_side === 'left') {
goal_side = 'right';
}
const mid_goal_offset = viewport_width * (1/24);
const new_left_offset = goal_side === 'left' ? mid_goal_offset - box_width/2 : viewport_width - mid_goal_offset - box_width/2;
object.style.transition = 'left 0.35s';
object.style.left = `${new_left_offset}px`;
}
function check_finished(){
let finished = false;
if (choice === 'together') {
if (partner_finished && participant_finished) {
finished = true;
}
}
if (choice === 'alone') {
if (partner_finished || participant_finished) {
finished = true;
}
}
if (finished) {
object_partner.removeEventListener('click', () => issue_alert('partner_click'));
object_participant.removeEventListener('click', () => issue_alert('early_click_choice'));
const save_data = {
choice: choice,
selector: trial.selector,
together_side: trial.together_side,
response_time: response_time,
}
setTimeout(() => {
jsPsych.finishTrial(save_data);
}, 500);
}
}
async function show_alert(message) {
const dialog = document.createElement("dialog");
document.body.appendChild(dialog);
dialog.className = "z-20 fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 p-4 bg-slate-100 text-red-700 rounded-lg shadow-lg";
dialog.innerText = message;
dialog.show();
setTimeout(function () {
dialog.close();
}, 2000);
}
function issue_alert(issue){
if (issue === 'partner_click'){
show_alert(`That is your partner's object. Please click the lower object.`)
}
if (issue === 'early_click_go' && !go_clicked){
show_alert(`You need to click 'Go' before clicking your object.`)
}
if (issue === 'early_click_choice'){
if (!partner_choice_made && trial.selector === 'partner'){
show_alert(`Please wait for your partner to make a choice.`)
}
if (!choice && trial.selector === 'participant'){
show_alert(`Please make a choice using the buttons below before clicking your object.`)
}
}
}
}
}
export default jsPsychObjectMoving;