import { AppUtils } from "src/app/app-utils";
import { QuizzPage } from "src/app/page/quizz/quizz.page";
import { QuizzAnswer, QuizzQuestion } from "./quizz-question";
import { ScenarioQuizz } from "./scenario-quizz";
import { LrsUtils } from "../lrs/lrsUtils";
import { Subject } from "rxjs";
import { Statement } from "../statement";
import { XapiObject, XObjectType } from "../lrs/xapiobject";
import { XapiContext, XapiExtensions } from "../lrs/xapicontext";
import { LrsVerbs } from "../lrs/xapiverbs";
import { XApiResult } from "../lrs/xapiresult";
import { Details } from "src/app/page/territoire/territoire.page";
import { OseDifficulty } from "../enums/oseDifficulty";
import { environment } from "src/environments/environment";
export enum Difficulty {
easy = 1,
medium = 2,
hard = 3
}
export enum ResultType {
success,
successOnRetry,
fail,
failOnFirstAttempt,
joker
}
export enum QuizzAnswerStatus {
valid,
rejected,
missing,
notValidAndUntouched
}
export enum ThermometerColor {
hell = "hell",
hot = "hot",
medium = "medium",
low = "low"
}
interface QuizzResult {
allValidated: boolean;
rejected: Array<number>;
validated: Array<number>;
missing: Array<number>;
}
export class Quizz {
id: number | string;
questions: Array<number>;
currentQuestion: QuizzQuestion;
skill: any;
group: string;
theme: string;
currentIndex = 0;
maxQuestions: number;
gaugeType: any;
difficulty: Difficulty;
// execution:
goodAnswersIds: number[];
userAnswer: number[];
goodAnswers: QuizzAnswer[];
answerSubject: Subject<boolean>;
quizzAnswersProcessed: Array<any>;
allQuestions: Array<QuizzQuestion>;
allQuestionsMode: boolean;
title: string;
questionsObjects: Array<QuizzQuestion>;
answerPromise: Promise<number[]>;
// lrs
timerUserResponse: number;
constructor(public page: QuizzPage, public randomMode: boolean, private scenario: ScenarioQuizz) {
this.id = null;
this.randomMode = randomMode;
}
public async runQuizz() {
if (this.randomMode) {
if (this.allQuestionsMode) {
AppUtils.shuffleArray(this.allQuestions);
} else {
AppUtils.shuffleArray(this.questions);
}
}
this.buildQuestionsObjects();
this.updateVariables();
if (!this.page.isComponent && !this.page.inJourney && this.id !== 36843 && this.id !== 36844) {
await this.scenario.playIntro();
}
this.newQuestion(true);
const statement = this.startActivityStatement();
if(statement){
this.page.lrs.send(statement);
}
}
buildQuestionsObjects() {
this.questionsObjects = new Array();
this.questions.forEach(qId => {
this.allQuestions.forEach(question => {
if (Number(qId) === Number(question.id)) {
this.questionsObjects.push(question);
}
});
});
}
/**
* quizz new question loop - game lifecyle
* @param firstTime true if first question
* @param idFromDev question id given in dev mode for testing
*/
public async newQuestion(firstTime?: boolean, idFromDev?: number) {
if (idFromDev) {
this.page.answerSubscription?.complete();
this.page.answerSubscription = new Subject<any>();
this.answerPromise = null;
}
this.page.ficheSkipped = false;
this.currentQuestion = this.getNextQuestion(idFromDev);
if (this.currentQuestion.background) {
this.page.updateBackground(this.currentQuestion.background);
}
// AppUtils.debug("currentQuestion = ", this.currentQuestion);
if (!this.page.isComponent){
await AppUtils.timeOut(1000);
}
if (this.page.currentTeam?.length > 1) {
await this.page.nextPlayer(firstTime);
}
this.answerPromise = this.page.waitUserAnswer();
const skipQuestion = (this.page.isComponent && !this.page.quizid) && this.page.difficultyWasteSelected === OseDifficulty.easy ? true : false
this.page.toggleQuizzPad(!skipQuestion);
this.buildCurrentQuestion();
this.page.displayConsigne(!skipQuestion);
const skipTTS = this.page.wasteObject && this.page.difficultyWasteSelected === OseDifficulty.easy
const askCurrentQuestionAndReadAnswersPromise = this.scenario.askCurrentQuestionAndReadAnswers(skipTTS);
this.page.padLocked = false;
this.timerUserResponse = Date.now();
if(skipQuestion){
this.page.receiveAnswer({code: this.goodAnswersIds,event: "skipMode"});
}
await this.answerPromise.then(async userAnswerArray => {
console.log(userAnswerArray);
this.userAnswer = userAnswerArray;
this.page.padLocked = true;
if (this.page.ttsService.isplaying) {
await this.page.skip();
}
await askCurrentQuestionAndReadAnswersPromise;
this.page.displayConsigne(false);
// debugger;
await this.manageAnswer(userAnswerArray, this.checkIfJoker(userAnswerArray));
await AppUtils.timeOut(1000);
if (this.isEndOfQuizz()) {
this.endQuizz();
} else {
if (this.id !== 36843 && this.id !== 36844 && this.page.currentTeam.length > 1) {
await this.page.nextPlayer();
}
this.newQuestion();
}
});
}
/**
* Test if end of quizz
*/
isEndOfQuizz(): boolean {
let end = this.currentIndex === this.questionsObjects.length ;
if(!environment.production){
// end = this.currentIndex === 1 ;
}
return end;
}
/**
* Execute end of quizz action
*/
async endQuizz() {
if (this.id == 36843 || this.id == 36844) {
this.page.outputEvent.next({ success: true, mesh: null});
} else if (!this.page.isComponent && !this.page.inJourney) {
await this.scenario.endOfActivity();
}
if(!this.page.isComponent && this.page.oseJourneyService.currentJourney){
if(this.page.oseJourneyService.currentJourney.isQuizzTypeStarted){
this.page.oseJourneyService.currentJourney.isQuizzTypeCompleted = true;
}
}
const endStatement = this.endActivityStatement();
if(endStatement){
this.page.lrs
.send(endStatement)
.then(() => {
if(this.page.isComponent){
if (this.page.isComponent) {
this.page.outputEvent.next({ success: this.page.currentUser.awardsCurrent.shooting > 0, mesh: this.page.wasteObject?.mesh});
}
} else {
this.page.oseJourneyService.exerciseEnd.next();
}
})
.catch(error => {
console.error("error last statement not send", error);
this.page.oseJourneyService.exerciseEnd.next(true);
});
}else{
if (this.page.isComponent) {
this.page.outputEvent.next({ success: this.page.currentUser.awardsCurrent.shooting > 0, mesh: this.page.wasteObject?.mesh});
}
}
}
/**
* check if the user answer is "I don't know"
* @param userAnswerArray user answer array (cause multi choice answers)
*/
checkIfJoker(userAnswerArray) {
const dontKnowAnswer = this.currentQuestion.answers.find(a => {
return a.text.toLowerCase() === "je ne sais pas";
});
let userDontKnow = false;
if (dontKnowAnswer) {
userDontKnow = userAnswerArray.some(a => dontKnowAnswer.id === a);
}
return userDontKnow;
}
// for dev
async nextQuestion() {
await this.page.skip();
this.newQuestion();
}
updateVariables() { }
/**
* get the next quizz question from this.questions depending on currentIndex (in normal mode)
* @param idFromDev question id for dev mode
*/
public getNextQuestion(idFromDev?: number) {
let questionObject;
if (this.allQuestionsMode) {
questionObject = this.allQuestions[idFromDev ? idFromDev : this.currentIndex];
} else {
questionObject = AppUtils.find(this.allQuestions, q => {
return Number(q.id) === Number(idFromDev ? idFromDev : this.questions[this.currentIndex]);
});
}
// remove empty answers:
questionObject.answers.forEach((answer, index) => {
if (answer.text === "") {
questionObject.answers.splice(index, 1);
}
});
if (!idFromDev) {
this.currentIndex++;
}
// AppUtils.debug("questionObject ", questionObject);
const question = Object.assign(new QuizzQuestion(), questionObject);
if ((questionObject as any).feedback_html) {
question.feedback = (questionObject as any).feedback_html;
}
question.answers.forEach((answer, index) => {
question.answers[index] = Object.assign(new QuizzAnswer(), answer);
});
this.page.cabriService.currentOseActivity = { ...this.page.cabriService.currentOseActivity, ...question, id: this.page.cabriService.currentOseActivity.id };
return question;
}
buildCurrentQuestion() {
this.goodAnswersIds = new Array();
this.goodAnswers = new Array();
this.page.updateBackground(this.currentQuestion.background);
this.currentQuestion.answers.forEach(answer => {
if (answer.isValid) {
this.goodAnswersIds.push(answer.id);
this.goodAnswers.push(answer);
}
});
// AppUtils.debug("this.goodAnswersIds = ", this.goodAnswersIds);
}
checkIfGoodResult(answersIds: any[]): QuizzResult {
const validated = new Array();
const rejected = new Array();
this.quizzAnswersProcessed = new Array();
let missing = this.goodAnswersIds;
answersIds.forEach(answerId => {
if (this.goodAnswersIds.indexOf(answerId) === -1) {
rejected.push(answerId);
} else {
validated.push(answerId);
missing = missing.filter(answer => {
return answer !== answerId;
});
}
});
let allValidated: boolean;
if (answersIds.length > 0 && validated.length === this.goodAnswersIds.length && validated.length === answersIds.length) {
// AppUtils.debug("all good !");
allValidated = true;
} else if (answersIds.length > 0 && validated.length === answersIds.length) {
// AppUtils.debug("answered good but missing other good answers ! ", missing);
} else if (validated.length > 0) {
// AppUtils.debug(`answered good but rejected other good answers ! validated : ${validated}, rejected : ${rejected}`);
} else {
// AppUtils.debug("only rejected = ", rejected);
}
const result: QuizzResult = {
allValidated,
rejected,
validated,
missing
};
this.createProcessedAnswers(result);
return result;
}
// updates numpad answers with result statuses
createProcessedAnswers(quizzResult: QuizzResult) {
this.currentQuestion.answers.forEach(answer => {
const answerStatus = this.getKeyByValue(quizzResult, answer.id) ? this.getKeyByValue(quizzResult, answer.id) : "untouched";
this.quizzAnswersProcessed.push({ id: answer.id, status: answerStatus });
});
// AppUtils.debug("this.quizzAnswersProcessed = ", this.quizzAnswersProcessed);
}
getKeyByValue(object, value) {
const keyFound = Object.keys(object).find(key => {
if (key !== "allValidated") {
const key2 = object[key];
return key2.includes(value);
}
});
return keyFound ? keyFound : null;
}
/**
*
* @param userAnswer processes user's answer depending of its status (remediation if false) + update thermometer score
* @param joker for "je ne sais pas" type of answer
*/
manageAnswer(userAnswer, joker: boolean): Promise<void> {
return new Promise(async (resolve, reject) => {
// this.page.needInitializeZoneResponse = true;
const timestampDiff = Date.now() - this.timerUserResponse;
const millis = Math.floor(timestampDiff / 1000);
LrsUtils.responseTime = millis;
this.page.displayBubble(false);
const result: QuizzResult = this.checkIfGoodResult(userAnswer);
if (result.allValidated || this.id == 36843 || this.id == 36844) {
// juste, passer à la question suivante
this.page.quizzPad?.clearAnswer();
this.page.toggleQuizzPad(false);
this.manageLRS(ResultType.success);
if (this.id !== 36843 && this.id !== 36844) {
this.page.updatePlayerAndTeamStarboards("shooting");
this.page.updateThermometer();
await this.scenario.goodResponseShooting();
} else {
resolve();
return;
}
await AppUtils.timeOut(500);
} else if (joker) {
// the user doesn't know
await this.scenario.dontKnowShowResults(this.goodAnswers.length > 1, false, 5000);
this.manageLRS(ResultType.joker);
this.page.updatePlayerAndTeamStarboards("normal");
this.page.updateThermometer();
this.page.quizzPad?.showPadResults(false);
this.page.quizzPad?.clearAnswer();
this.page.toggleQuizzPad(false);
} else {
this.manageLRS(ResultType.fail);
this.page.updatePlayerAndTeamStarboards("moon");
this.page.updateThermometer();
if (this.goodAnswers.length > 1) {
// bad answer - there was several good answers
if (result.validated.length > 0 && result.rejected.length === 0) {
await this.scenario.badAnswerCustomAndShowResults(
$localize`Oui ! Mais il y avait plusieurs bonnes réponses, regarde !`,
false,
5000
);
} else if (result.validated.length > 0 && result.rejected.length > 0) {
await this.scenario.badAnswerCustomAndShowResults(
$localize`Oups ! les bonnes réponses étaient celles-ci, regarde !`,
false,
5000
);
} else {
await this.scenario.badAnswerCustomAndShowResults(
$localize`Oups ! Il y avait plusieurs bonnes réponses, regarde !`,
false,
5000
);
}
} else {
// bad answer - there was only one good answer
await this.scenario.badAnswerCustomAndShowResults(
$localize`Oups ! La bonne réponse était celle-ci, regarde !`,
false,
5000
);
}
this.page.quizzPad?.showPadResults(false);
this.page.quizzPad?.clearAnswer();
this.page.toggleQuizzPad(false);
}
if(!this.page.nofeedback){
await this.displayAndReadFeedback();
}
resolve();
});
}
/**
* displays feedback from fiche component
*/
async displayAndReadFeedback() {
this.page.displayFeedback(true);
await this.scenario.readFeedbackFiche(this.currentQuestion.feedback);
this.page.displayFeedback(false);
}
manageLRS(result: ResultType) {
let statement: Statement;
switch (result) {
case ResultType.success:
statement = this.page.lrs.oseQuestionStatement(LrsVerbs.passed,this.lrsFormatParticipantAnswer);
// this.page.updatePlayerAndTeamStarboards("shooting");
break;
case ResultType.successOnRetry:
statement = this.page.lrs.oseQuestionStatement(LrsVerbs.passedWithHelp,this.lrsFormatParticipantAnswer);
// this.page.updatePlayerAndTeamStarboards("normal");
break;
case ResultType.fail:
statement = this.page.lrs.oseQuestionStatement(LrsVerbs.failed,this.lrsFormatParticipantAnswer);
// this.page.updatePlayerAndTeamStarboards("moon");
break;
case ResultType.failOnFirstAttempt:
statement = this.page.lrs.oseQuestionStatement(LrsVerbs.failedOnFirstAttempt,this.lrsFormatParticipantAnswer);
break;
case ResultType.joker:
statement = this.page.lrs.oseQuestionStatement(LrsVerbs.skipped,this.lrsFormatParticipantAnswer);
break;
}
console.log("statement = ", statement);
this.page.lrs.send(statement).then((sendedStatement: Statement) => {
sendedStatement.setVerb(LrsVerbs.finished);
this.page.lrs.send(sendedStatement);
});
}
public get lrsFormatParticipantAnswer(): { id: string; name: string }[] {
return this.userAnswer.map(answerNumber => {
const answer = this.currentQuestion.answers[answerNumber - 1];
return { id: String(answer.id), name: answer.text }; // Convertir l'ID en chaîne
});
}
/**
* Statement object created at the end of the activity
* @returns Statement[] contains one or two elements depending if it's the last exercise of journey (ex + journey) or a simple exercise(1)
*/
endActivityStatement(): Statement[] {
if (this.id == 36843 || this.id == 36844) {
return;
} else if(!this.page.wasteObject){
const statements = new Array();
const userAwards = this.page.currentUser.awardsCurrent;
const perGoodAnswers = ((userAwards.shooting + userAwards.normal) / userAwards.total) * 100;
const perWrongAnswers = (userAwards.moons / userAwards.total) * 100;
if(this.page.oseJourneyService.currentJourney){
this.page.oseJourneyService.currentJourney.calculateSessionExerciseCompletionPercentage();
}
const object = new XapiObject(`${LrsUtils.statementUrl}/${String(this.id)}`, this.title, this.theme, XObjectType.exercise);
const activityDuration = LrsUtils.duration8601(LrsUtils.timeStampConversion(LrsUtils.currentUserResponseTime));
const result = new XApiResult(LrsVerbs.completed, activityDuration);
const context = new XapiContext(this.page.accountService.team, {
...this.page.lrs.globalActivityExtensions(),
[XapiExtensions.pourcentageBonnesReponses]: perGoodAnswers,
[XapiExtensions.pourcentageMauvaisesReponses]: perWrongAnswers,
[XapiExtensions.dureeActivite]: activityDuration,
[XapiExtensions.sequenceTermine]: !this.page.oseJourneyService.currentJourney?.isQuizzTypeCompleted ? true : false
});
const exerciseStatement = new Statement(this.page.currentUser?.id, LrsVerbs.completed, object, context, result);
const endJourneyStatement = this.page.lrs.endJourneyStatement();
if (endJourneyStatement) {
statements.push(endJourneyStatement);
}
statements.push(exerciseStatement);
return statements;
}
}
/**
* Statement object created at the beginning of the activity.
* @returns Statement[] contains one or two elements depending if it's a journey(ex + journey) or a simple exercise(1)
*/
startActivityStatement(): Statement[] {
if(!this.page.wasteObject){
const statements = new Array();
LrsUtils.idsession = LrsUtils.generateUniqueSessionId(this.page.accountService, true);
LrsUtils.currentUserResponseTime = Date.now();
const object = new XapiObject(`${LrsUtils.statementUrl}/exercise/${String(this.id)}`, this.title, this.theme, XObjectType.exercise);
const context = new XapiContext(this.page.accountService.team, {
...this.page.lrs.globalActivityExtensions()
});
const exerciseStatement = new Statement(this.page.currentUser?.id, LrsVerbs.initialized, object, context);
const startJourneyStatement = this.page.lrs.startJourneyStatement();
if (startJourneyStatement) {
statements.push(startJourneyStatement);
}
statements.push(exerciseStatement);
return statements;
}
}
}