{"version":3,"file":"app-4e803688.f584a1446b2fc034d810.bundle.js","mappings":"AAGA","sources":["webpack://lernplattform-admin-frontend/../common/aurelia-components/possible-answers-classic/possible-answers-classic.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/possible-answers/possible-answers.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/progress-bar/progress-bar.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/qr-code-scanner/qr-code-scanner.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/quiz-result-question-answer/quiz-result-question-answer.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/quiz-result-question-block/quiz-result-question-block.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/recent-trainings-list/recent-trainings-list.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/rich-text/components/ias-jump-link.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/rich-text/components/internal-link.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/rich-text/rich-text.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/risi-game/risi-controls-b/risi-controls-b.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/risi-game/risi-course-learn-page-header/risi-course-learn-page-header.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/risi-game/risi-game.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/risi-game/risi-graphic/risi-graphic.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/risi-game/risi-learn-end-card/risi-learn-end-card.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/risi-game/risi-learn-end-page-content/risi-learn-end-page-content.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/risi-game/risi-learn-page/risi-learn-page.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/risi-game/risi-map-marker/risi-map-marker.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/risi-game/risi-map/risi-map.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/rounded-progress-bar/rounded-progress-bar.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/possible-answers-classic/index.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/possible-answers-classic/possible-answers-classic.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/possible-answers/index.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/possible-answers/possible-answers.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/progress-bar/index.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/progress-bar/progress-bar.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/qr-code-scanner/index.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/qr-code-scanner/qr-code-scanner.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/quiz-result-question-answer/index.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/quiz-result-question-answer/quiz-result-question-answer.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/quiz-result-question-block/index.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/quiz-result-question-block/quiz-result-question-block.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/recent-trainings-list/index.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/recent-trainings-list/recent-trainings-list.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/rich-text/components/ias-jump-link.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/rich-text/components/internal-link.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/rich-text/index.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/rich-text/rich-text.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/risi-game/colors.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/risi-game/index.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/risi-game/risi-controls-b/risi-controls-b.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/risi-game/risi-course-learn-page-header/risi-course-learn-page-header.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/risi-game/risi-game.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/risi-game/risi-graphic/risi-graphic.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/risi-game/risi-learn-end-card/risi-learn-end-card.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/risi-game/risi-learn-end-page-content/risi-learn-end-page-content.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/risi-game/risi-learn-page/risi-learn-page.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/risi-game/risi-map-marker/risi-map-marker.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/risi-game/risi-map/risi-map.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/rounded-progress-bar/index.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/rounded-progress-bar/rounded-progress-bar.ts"],"sourcesContent":["// Module\nvar code = \"<template class=\\\"possible-answers-classic\\\"> <button-exam-classic repeat.for=\\\"option of answerOptions\\\" answer-id.to-view=\\\"option.answerId\\\" selected.to-view=\\\"isSelected(option.answerId, selectedAnswers)\\\" btn-id.to-view=\\\"option.buttonId\\\" btn-text.to-view=\\\"option.text\\\" data-test-selector=\\\"option.dataTestSelector\\\" disabled.to-view=\\\"disabled\\\"></button-exam-classic> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"possible-answers possible-answers--${theme} ${answerOptions[0].pictureUrl ? 'possible-answers--with-images' : ''} possible-answers--type-${questionType} possible-answers--responsiveness-${responsiveness} possible-answers--contrast-${contrastMode} possible-answers--answer-tiling-${answerTilingMode} ${ verticalDisplayMode === VerticalDisplayMode.Scroll ? 'possible-answers--height-from-children' : ''}\\\"> <button-exam repeat.for=\\\"option of answerOptions\\\" class=\\\"possible-answers__button button-exam--answer-tiling-${answerTilingMode}\\\" contrast-mode.to-view=\\\"contrastMode\\\" uppercase.to-view=\\\"isUppercase\\\" answer-id.to-view=\\\"option.answerId\\\" letter.to-view=\\\"option.letter\\\" selected.to-view=\\\"isSelected(option.answerId, selectedAnswers)\\\" btn-id.to-view=\\\"option.buttonId\\\" btn-text.to-view=\\\"option.text\\\" btn-picture-url.to-view=\\\"option.pictureUrl\\\" btn-picture-blur-hash.to-view=\\\"option.pictureBlurHash\\\" btn-audio-url.to-view=\\\"option.audioUrl\\\" is-audio-answer.to-view=\\\"option.isAudioAnswer\\\" data-test-selector=\\\"option.dataTestSelector\\\" disabled.to-view=\\\"disabled\\\" theme.to-view=\\\"theme\\\" responsiveness.to-view=\\\"responsiveness\\\" correct.to-view=\\\"option.isCorrect\\\" revealed.to-view=\\\"revealAnswer\\\" readonly.to-view=\\\"readonly\\\" vertical-display-mode.to-view=\\\"verticalDisplayMode\\\" voice-over-enabled.to-view=\\\"voiceOverEnabled\\\"></button-exam> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"progress-bar--${type} ${progress < 0 ? `progress-bar--negative` : ``}\\\"> <div class=\\\"progress-bar__empty-bar ${backgroundColor == null ? 'progress-bar__empty-bar--background-default' : ''}\\\" style.to-view=\\\"style\\\"></div> <div class=\\\"progress-bar__progress ${transition ? 'progress-bar__progress--transition' : ''}\\\" style.to-view=\\\"getProgressStyle(progress)\\\" data-test-selector=\\\"progress-bar-progress\\\"></div> </template> \";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"qr-code-scanner\\\"> <video class=\\\"qr-code-scanner__video\\\" ref=\\\"videoElement\\\" disablePictureInPicture></video> <safe-icon if.to-view=\\\"loadingState === LoadingState.Error\\\" class=\\\"qr-code-scanner__error-icon\\\" icon-name=\\\"System/AlertCircle\\\"></safe-icon> <loading-overlay visible.to-view=\\\"loadingState === LoadingState.Loading\\\"></loading-overlay> <div if.to-view=\\\"errorTk\\\" class=\\\"qr-code-scanner__error\\\" t.to-view=\\\"errorTk\\\"></div> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"quiz-result-question-answer ${hasSecondaryAnswers ? 'quiz-result-question-answer--two-icons' : ''}\\\"> <div class=\\\"quiz-result-question-answer__icon-wrapper\\\"> <x-icon class=\\\"quiz-result-question-answer__icon\\\" icon-type.to-view=\\\"`custom`\\\" icon-name.to-view=\\\"getResultIcon(answerSelected, answerCorrect)\\\"> </x-icon> <div class=\\\"quiz-result-question-answer__help\\\"> <p class=\\\"quiz-result-question-answer__help-text\\\" t=\\\"${getResultIconHelpTk(answerSelected, answerCorrect)}\\\"></p> </div> </div> <div if.to-view=\\\"hasSecondaryAnswers\\\" class=\\\"quiz-result-question-answer__icon-wrapper\\\"> <x-icon class=\\\"quiz-result-question-answer__icon\\\" icon-type.to-view=\\\"`custom`\\\" icon-name.to-view=\\\"getResultIcon(secondaryAnswerSelected, secondaryAnswerCorrect)\\\"> </x-icon> <div class=\\\"quiz-result-question-answer__help\\\"> <p class=\\\"quiz-result-question-answer__help-text\\\" t=\\\"${getResultIconHelpTk(secondaryAnswerSelected, secondaryAnswerCorrect)}\\\"></p> </div> </div> <span class=\\\"quiz-result-question-answer__label\\\">${label}</span> <span if.bind=\\\"!pictureUrl\\\" class=\\\"quiz-result-question-answer__text\\\">${text}</span> <img if.bind=\\\"pictureUrl\\\" class=\\\"quiz-result-question-answer__picture\\\" alt.to-view=\\\"text\\\" src.to-view=\\\"pictureUrl\\\"/> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"quiz-result-question-block\\\"> <header class=\\\"quiz-result-question-block__header\\\"> <div class=\\\"quiz-result-question-block__header-line\\\"> <span if.to-view=\\\"question.importId\\\" class=\\\"quiz-result-question-block__question-index\\\" t=\\\"${processTranslation(`question`)}\\\" t-params.to-view=\\\"{importId: question.importId}\\\"></span> <span else class=\\\"quiz-result-question-block__question-index\\\" t=\\\"${processTranslation(`question`)}\\\" t-params.to-view=\\\"{importId: currentQuestionIndex + 1}\\\"></span> <button-standard if.to-view=\\\"page\\\" class=\\\"quiz-result-question-block__jump-to-page\\\" btn-type.to-view=\\\"`overline`\\\" btn-shape.to-view=\\\"`condensed`\\\" icon-type=\\\"custom\\\" icon-name=\\\"System/Jump\\\" click.delegate=\\\"jumpToPage()\\\" t=\\\"[btnText]${processTranslation(`jumpToPage`)}\\\"></button-standard> <span if.to-view=\\\"!question.isAnswered\\\" class=\\\"quiz-result-question-block__question-result quiz-result-question-block__question-result--timeout\\\" t=\\\"${processTranslation(`timeout`)}\\\"></span> <span if.to-view=\\\"question.isAnswered && question.isCorrect\\\" class=\\\"quiz-result-question-block__question-result quiz-result-question-block__question-result--correct\\\" t=\\\"${processTranslation(`correct`)}\\\"></span> <span if.to-view=\\\"question.isAnswered && !question.isCorrect && question.isHalfCorrect\\\" class=\\\"quiz-result-question-block__question-result quiz-result-question-block__question-result--half-correct\\\" t=\\\"${processTranslation(`halfCorrect`)}\\\"></span> <span if.to-view=\\\"question.isAnswered && !question.isCorrect && !question.isHalfCorrect\\\" class=\\\"quiz-result-question-block__question-result quiz-result-question-block__question-result--wrong\\\" t=\\\"${processTranslation(`wrong`)}\\\"></span> </div> <div if.to-view=\\\"hasSubQuestion || isSubQuestion || showDifficultyAndPoints\\\" class=\\\"quiz-result-question-block__header-line\\\"> <span if.to-view=\\\"hasSubQuestion || isSubQuestion\\\" class=\\\"quiz-result-question-block__question-sub-question-info\\\" t=\\\"${processTranslation(isSubQuestion ? `subQuestion` : `mainQuestion`)}\\\"></span> <span if.to-view=\\\"showDifficultyAndPoints\\\" class=\\\"quiz-result-question-block__question-difficulty\\\"> <span if.to-view=\\\"!isPracticeExam\\\" t=\\\"${processTranslation(`difficulty-${difficulty}`)}\\\"></span> <span if.to-view=\\\"isPracticeExam\\\" t=\\\"${processTranslation(`achievedPoints`)}\\\" t-params.to-view=\\\"{achieved: question.achievedPoints, max: question.maxPoints, count: question.maxPoints}\\\"></span> <span else> (<span t=\\\"${processTranslation(`points`)}\\\" t-params.to-view=\\\"{count: question.points}\\\"></span>) </span> </span> </div> </header> <main class=\\\"quiz-result-question-block__main\\\"> <p if.to-view=\\\"question.type !== QuestionType.LT\\\" class=\\\"quiz-result-question-block__question\\\">${question.text}</p> <picture-puzzle if.to-view=\\\"question.type === QuestionType.SCP || question.type === QuestionType.MCP\\\" class=\\\"quiz-result-question-block__question-image\\\" question.to-view=\\\"question\\\" selected-answers.to-view=\\\"getAnswerArray(question.answers[0])\\\" disabled.to-view=\\\"true\\\" reveal-answer.to-view=\\\"true\\\" readonly.to-view=\\\"true\\\"></picture-puzzle> <template else> <exam-image alt-text.to-view=\\\"question.text\\\" blink-img-src.to-view=\\\"question.pictureBlinkUrl\\\" blur-hash.to-view=\\\"question.pictureBlurHash\\\" blink-enabled.to-view=\\\"blinkEnabled\\\" blink-blur-hash.to-view=\\\"question.pictureBlinkBlurHash\\\" class=\\\"quiz-result-question-block__question-image\\\" if.to-view=\\\"question.pictureUrl\\\" img-src.to-view=\\\"question.pictureUrl\\\"></exam-image> <div repeat.for=\\\"answer of question.answers\\\" class=\\\"quiz-result-question-block__question-container\\\"> <p if.to-view=\\\"question.type === QuestionType.LT\\\" class=\\\"quiz-result-question-block__question quiz-result-question-block__question--fitb\\\">${fitbTexts[$index]} </p> <div class=\\\"quiz-result-question-block__answers ${answer.pictureUrl1 && answer.pictureUrl2 && answer.pictureUrl3 && answer.pictureUrl4 ? 'quiz-result-question-block__answers--images' : ''}\\\"> <quiz-result-question-answer answer-selected.to-view=\\\"answer.isSelected1\\\" answer-correct.to-view=\\\"answer.isCorrect1\\\" secondary-answer-selected.to-view=\\\"secondaryAnswers[$index].isSelected1\\\" secondary-answer-correct.to-view=\\\"secondaryAnswers[$index].isCorrect1\\\" label.to-view=\\\"`A`\\\" text.to-view=\\\"getAnswerTextOrTranslation(answer.text1, processTranslation(`true`))\\\" picture-url.to-view=\\\"answer.pictureUrl1\\\"></quiz-result-question-answer> <quiz-result-question-answer answer-selected.to-view=\\\"answer.isSelected2\\\" answer-correct.to-view=\\\"answer.isCorrect2\\\" secondary-answer-selected.to-view=\\\"secondaryAnswers[$index].isSelected2\\\" secondary-answer-correct.to-view=\\\"secondaryAnswers[$index].isCorrect2\\\" label.to-view=\\\"`B`\\\" text.to-view=\\\"getAnswerTextOrTranslation(answer.text2, processTranslation(`false`))\\\" picture-url.to-view=\\\"answer.pictureUrl2\\\"></quiz-result-question-answer> <quiz-result-question-answer if.to-view=\\\"question.type !== QuestionType.TF\\\" answer-selected.to-view=\\\"answer.isSelected3\\\" answer-correct.to-view=\\\"answer.isCorrect3\\\" secondary-answer-selected.to-view=\\\"secondaryAnswers[$index].isSelected3\\\" secondary-answer-correct.to-view=\\\"secondaryAnswers[$index].isCorrect3\\\" label.to-view=\\\"`C`\\\" text.to-view=\\\"answer.text3\\\" picture-url.to-view=\\\"answer.pictureUrl3\\\"></quiz-result-question-answer> <quiz-result-question-answer if.to-view=\\\"question.type !== QuestionType.TF\\\" answer-selected.to-view=\\\"answer.isSelected4\\\" answer-correct.to-view=\\\"answer.isCorrect4\\\" secondary-answer-selected.to-view=\\\"secondaryAnswers[$index].isSelected4\\\" secondary-answer-correct.to-view=\\\"secondaryAnswers[$index].isCorrect4\\\" label.to-view=\\\"`D`\\\" text.to-view=\\\"answer.text4\\\" picture-url.to-view=\\\"answer.pictureUrl4\\\"></quiz-result-question-answer> </div> </div> </template> </main> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"recent-trainings-list lp-site-content-small-no-padding\\\"> <div class=\\\"lp-site-content-small recent-trainings-list__title ${ centerTitle ? 'recent-trainings-list__title--center' : '' }\\\" t=\\\"common.recent-training-list.${ isFrp ? 'title' : 'titleExcercise' }\\\"></div> <list-standard class=\\\"course-select-dialog__list\\\" items.to-view=\\\"listItems\\\"></list-standard> <button-standard if.to-view=\\\"recentTrainingsCount > recentTrainingsList.length\\\" click.delegate=\\\"loadAdditional()\\\" class=\\\"recent-trainings-list__btn\\\" btn-type=\\\"text\\\" icon-type=\\\"custom\\\" icon-name=\\\"System/Refresh\\\" t=\\\"[btnText]common.recent-training-list.loadAdditional\\\"></button-standard> <div if.to-view=\\\"!recentTrainingsCount\\\" class=\\\"recent-trainings-list__empty lp-site-content-small\\\" t=\\\"common.recent-training-list.empty\\\"></div> <loading-overlay visible.to-view=\\\"loading\\\"></loading-overlay> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"ias-jump-link\\\"> <span click.delegate=\\\"jump()\\\" class=\\\"${classList}\\\">${text}</span> </template> \";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"internal-link\\\"> <a if.to-view=\\\"linkTitle\\\" use-as-button click.delegate=\\\"handleLinkClick()\\\"> <safe-icon class=\\\"internal-link__link-icon\\\" icon-name=\\\"System/Jump\\\"></safe-icon>${linkTitle} </a> <span else t=\\\"common.internal-link.broken\\\"></span> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"rich-text ${ isEmpty ? 'rich-text--empty' : '' }\\\"> <div show.to-view=\\\"!isEmpty\\\" ref=\\\"editorRef\\\" aria-label.to-view=\\\"plainText\\\" class=\\\"rich-text__editor rich-text__editor--${mode}\\\"></div> <div if.to-view=\\\"isEmpty\\\" class=\\\"rich-text__placeholder\\\" t.to-view=\\\"placeholderTk\\\"></div> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"risi-controls-b ${hoverMode ? 'risi-controls-b--hover-mode' : ''}\\\"> <div class=\\\"risi-controls-b__button risi-controls-b__button--undo ${undoAvailable ? '' : 'risi-controls-b__button--disabled'} risi-game__circle-button\\\" click.delegate=\\\"handleUndoClicked()\\\"> <x-icon class=\\\"risi-controls-b__button-icon risi-controls-b__button-icon--undo\\\" size=\\\"unspecified\\\" icon-type=\\\"custom\\\" icon-name=\\\"Risi/Undo\\\"></x-icon> </div> <div class=\\\"risi-controls-b__action-container\\\" ref=\\\"actionScrollWrapper\\\" mousemove.trigger=\\\"handleMouseMove($event) & throttle:50\\\"> <div class=\\\"risi-controls-b__scroll-wrapper\\\" style.to-view=\\\"{'--total-cards': playerMoves.length}\\\"> <div repeat.for=\\\"move of playerMoves\\\" class=\\\"risi-controls-b__action-card risi-controls-b__action-card--action-${move} ${chosenAction === move ? 'risi-controls-b__action-card--selected' : ''}\\\" style.to-view=\\\"{left: `${hoverModeOffsets[$index] || 0}px`, '--sign-distance-from-center': Math.sign(hoveredCardIndex-$index), '--abs-distance-from-center': Math.abs(hoveredCardIndex-$index) }\\\" click.delegate=\\\"handleActionClicked(move)\\\"> <img class=\\\"risi-controls-b__action-icon ${chosenAction === move && executingMove ? 'risi-controls-b__action-icon--used' : ''}\\\" src.to-view=\\\"moveIcons[move]\\\"> <div class=\\\"risi-controls-b__action-label\\\" t.to-view=\\\"`common.risi-game.moves.${move}`\\\"></div> </div> </div> </div> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"risi-course-learn-page-header\\\"> <span if.to-view=\\\"chapterEnd\\\" t=\\\"common.course-learn-page-header.chapterEnd\\\"></span> <span else t=\\\"common.risi-game.pageXofY\\\" t-params.to-view=\\\"{\\n          pageNumber: navigationInfo.pageIndexInChapter,\\n          pagesTotal: navigationInfo.totalChapterPages\\n        }\\\"></span> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"risi-game ${blink ? 'risi-game--blink' : ''} ${fadeout ? 'risi-game--fadeout' : ''}\\\"> <div class=\\\"risi-game__grid\\\" style.to-view=\\\"getGridStyle(config)\\\"> <template repeat.for=\\\"layer of tiles\\\"> <div repeat.for=\\\"tile of layer\\\" class=\\\"risi-game__tile\\\" style.to-view=\\\"getGridCoords(tile.coords)\\\"> <risi-graphic class=\\\"risi-game__tile-graphic\\\" graphic.to-view=\\\"tile.graphic\\\"></risi-graphic> </div> </template> <template repeat.for=\\\"y of config.rows\\\"> <template repeat.for=\\\"x of config.columns\\\"> <div class=\\\"risi-game__grid-cell\\\" style.to-view=\\\"getGridCoords({x,y})\\\"></div> </template> </template> <div repeat.for=\\\"sprite of actorSprites\\\" class=\\\"risi-game__actor\\\" style.to-view=\\\"getAbsoluteCoords(sprite.coords)\\\"> <risi-graphic class=\\\"risi-game__actor-graphic ${getGraphicClass(sprite.baseClass, sprite.modifiers)}\\\" graphic.to-view=\\\"sprite.graphic\\\"></risi-graphic> </div> <div repeat.for=\\\"sprite of decoSprites\\\" class=\\\"risi-game__actor\\\" style.to-view=\\\"getAbsoluteCoords(sprite.coords)\\\"> <risi-graphic class=\\\"risi-game__actor-graphic ${getGraphicClass(sprite.baseClass, sprite.modifiers)}\\\" graphic.to-view=\\\"sprite.graphic\\\"></risi-graphic> </div> </div> <div class=\\\"risi-game__controls-button risi-game__controls-button--exit risi-game__circle-button\\\" click.delegate=\\\"showExitDialog()\\\"> <x-icon class=\\\"risi-game__controls-button-icon risi-game__controls-button-icon--exit\\\" size=\\\"unspecified\\\" icon-type=\\\"custom\\\" icon-name=\\\"Risi/Back\\\"></x-icon> </div> <div class=\\\"risi-game__controls-button risi-game__controls-button--auto-play risi-game__circle-button\\\" click.delegate=\\\"startAutoPlay()\\\"> <x-icon class=\\\"risi-game__controls-button-icon risi-game__controls-button-icon--auto-play\\\" size=\\\"unspecified\\\" icon-type=\\\"custom\\\" icon-name=\\\"Risi/Bulb\\\"></x-icon> </div> <risi-controls-b if.to-view=\\\"controlType === 'B'\\\" class=\\\"risi-game__controls\\\" view-model.ref=\\\"controls\\\" executing-move.to-view=\\\"executingMove\\\" player-moves.to-view=\\\"playerMoves\\\" move-icons.to-view=\\\"moveIcons\\\" undo-available.to-view=\\\"undoAvailable\\\" risi-controls-reset-clicked.trigger=\\\"resetLevel()\\\" risi-controls-undo-clicked.trigger=\\\"handleUndoClicked()\\\" risi-controls-play-clicked.trigger=\\\"handlePlayClicked($event)\\\"></risi-controls-b> <div if.to-view=\\\"dialogModel\\\" class=\\\"risi-game__dialog-container\\\"> <div class=\\\"risi-game__dialog-content\\\"> <h2 class=\\\"risi-game__dialog-title\\\" t.to-view=\\\"dialogModel.titleTk\\\"></h2> <p repeat.for=\\\"descriptionTk of dialogModel.descriptionTks\\\" class=\\\"risi-game__dialog-description\\\" t.to-view=\\\"descriptionTk\\\"></p> <div class=\\\"risi-game__dialog-buttons\\\"> <button-standard repeat.for=\\\"button of dialogModel.buttons\\\" class=\\\"risi-game__dialog-button\\\" icon-name.to-view=\\\"button.iconName\\\" icon-type=\\\"custom\\\" btn-type.to-view=\\\"button.type\\\" btn-shape=\\\"condensed\\\" t=\\\"[btnText]${button.labelTk}\\\" click.trigger=\\\"answerDialog(button.value)\\\"></button-standard> </div> </div> </div> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"risi-graphic\\\" style.to-view=\\\"getStyle(graphic)\\\"> </template> \";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"risi-learn-end-card\\\"> <div class=\\\"risi-learn-end-card__header\\\"> <own-avatar-icon class=\\\"risi-learn-end-card__avatar\\\" size=\\\"small\\\"></own-avatar-icon> </div> <div class=\\\"risi-learn-end-card__content\\\"> <div class=\\\"risi-learn-end-card__title\\\" t=\\\"common.risi-game.content.wellDone\\\"></div> <div class=\\\"risi-learn-end-card__title\\\" t=\\\"common.risi-game.content.contentFinished\\\"></div> </div> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"risi-learn-end-page-content\\\"> <risi-course-learn-page-header context-identifier.to-view=\\\"contextIdentifier\\\" progress.to-view=\\\"navPreview ? null : progress\\\" page-id.to-view=\\\"pageId\\\" chapter-end.to-view=\\\"true\\\"></risi-course-learn-page-header> <div class=\\\"risi-learn-end-page-content__main lp-site-content-small\\\"> <div class=\\\"risi-learn-end-page-content__inner lp-site-content-small\\\"> <risi-learn-end-card class=\\\"risi-learn-end-page-content__card\\\" nav-preview.to-view=\\\"navPreview\\\"></risi-learn-end-card> </div> </div> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"risi-learn-page__wrapper\\\"> <template repeat.for=\\\"page of preparedPages\\\" if.to-view=\\\"contextIdentifier != null\\\"> <course-learn-page-content if.to-view=\\\"typeof page.pageId === 'number'\\\" attach-to.to-view=\\\"{previousSlot, currentSlot, nextSlot}[page.attachTo]\\\" class=\\\"risi-learn-page__page\\\" nav-preview.to-view=\\\"page.attachTo !== 'currentSlot'\\\" progress.to-view=\\\"page.attachTo === 'currentSlot' ? progress : null\\\" context-identifier.to-view=\\\"contextIdentifier\\\" page-id.to-view=\\\"page.pageId\\\" header-view-model.to-view=\\\"headerViewModel\\\" header-class=\\\"risi-course-learn-page-header\\\"></course-learn-page-content> <risi-learn-end-page-content if.to-view=\\\"page.pageId === 'end-page'\\\" class=\\\"risi-learn-page__page\\\" attach-to.to-view=\\\"{previousSlot, currentSlot, nextSlot}[page.attachTo]\\\" page-id.to-view=\\\"rootPageId\\\" nav-preview.to-view=\\\"page.attachTo !== 'currentSlot'\\\"></risi-learn-end-page-content> </template> <div class=\\\"risi-learn-page__header\\\"></div> <navigation-slider class=\\\"risi-learn-page__navigation-slider\\\" min.to-view=\\\"1\\\" max.to-view=\\\"navigationInfo.totalChapterPages\\\" value.to-view=\\\"(endPageShown ? navigationInfo.totalChapterPages + 1 : navigationInfo.pageIndexInChapter) + currentSwipeValue\\\"></navigation-slider> <swipe-nav view-model.ref=\\\"swipeNav\\\" navigate-back.trigger=\\\"navigateBack()\\\" navigate-forward.trigger=\\\"navigateForward()\\\" swiping.trigger=\\\"handleSwiping($event)\\\"> <div slot=\\\"previous\\\" class=\\\"risi-learn-page\\\" ref=\\\"previousSlot\\\"></div> <div slot=\\\"current\\\" class=\\\"risi-learn-page\\\" ref=\\\"currentSlot\\\"></div> <div slot=\\\"next\\\" class=\\\"risi-learn-page\\\" ref=\\\"nextSlot\\\"></div> </swipe-nav> <course-learn-footer class=\\\"risi-learn-page__footer\\\"></course-learn-footer> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"risi-map-marker risi-map-marker--${state} ${isGame ? 'risi-map-marker--game' : ''}\\\"> <div class=\\\"risi-map-marker__circle-button risi-map-marker__level-button\\\"> <slot></slot> </div> <risi-graphic if.to-view=\\\"isCurrent\\\" class=\\\"risi-map-marker__current-marker risi-map-marker__current-marker--helmet-${helmetType}\\\" graphic.to-view=\\\"avatarGraphic\\\"></risi-graphic> <risi-graphic if.to-view=\\\"state === CourseMapMarkerState.FINISHED\\\" class=\\\"risi-map-marker__finished-marker\\\" graphic.to-view=\\\"checkGraphic\\\"></risi-graphic> </template> \";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"risi-map\\\"> <div class=\\\"risi-map__container\\\"> <img class=\\\"risi-map__map-image\\\" src.to-view=\\\"'risi/mainmap.png'\\\"> <risi-graphic class=\\\"risi-map__map-overlay\\\" graphic.to-view=\\\"mapObjectsGraphic\\\"></risi-graphic> <risi-map-marker repeat.for=\\\"chapter of chaptersInMap\\\" ref=\\\"chapterCircleElements[$index]\\\" css=\\\"--risi-map-marker--x: ${chapter.coords.x}%; --risi-map-marker--y: ${chapter.coords.y}%\\\" click.delegate=\\\"handleChapterClick(chapter)\\\" state.to-view=\\\"chapter.state\\\" shirt-color-light.to-view=\\\"shirtColorLight\\\" shirt-color-shade.to-view=\\\"shirtColorShade\\\" helmet-type.to-view=\\\"helmetType\\\" helmet-color.to-view=\\\"helmetColor\\\" is-game.to-view=\\\"chapter.isGame\\\" is-current.to-view=\\\"chapterIsCurrent(chapter.state, $index, progress)\\\">${chapter.label}</risi-map-marker> </div> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"rounded-progress-bar rounded-progress-bar--color-${progressColor}\\\" role=\\\"progressbar\\\" aria-valuenow.to-view=\\\"progress\\\" aria-valuemin.to-view=\\\"minProgress\\\" aria-valuemax.to-view=\\\"maxProgress\\\"> <div class=\\\"rounded-progress-bar__actual-progress\\\"></div> </template>\";\n// Exports\nexport default code;","import { PLATFORM, FrameworkConfiguration } from 'aurelia-framework';\n\nexport function configure(config: FrameworkConfiguration): void {\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/possible-answers-classic/possible-answers-classic'));\n}\n","import { autoinject, bindable, computedFrom, PLATFORM, useView } from 'aurelia-framework';\nimport { IAnswerTranslationDto } from 'common/dto/course/content/answer.interface';\nimport { IQuizAnswerDto } from 'common/dto/course/quiz/quiz.answer.interface';\nimport { QuestionUtils } from 'common/utils/QuestionUtils';\n\ninterface IPossibleClassicAnswerOption {\n  answerId: number;\n  buttonId: string;\n  text: string;\n  dataTestSelector: string;\n  isCorrect?: boolean;\n}\n\nexport enum AnswerTilingMode {\n  Default = 'default',\n  OneByFourTexts = 'one-by-four-texts',\n}\n\n/**\n * css variables:\n * |name|description|\n * |----|-----------|\n * |--possible-answers-scale-factor|scaling factor passed through to buttons|\n */\n@autoinject()\n@useView(PLATFORM.moduleName('./possible-answers-classic.html'))\nexport class PossibleAnswersClassic {\n  @bindable() public answers: IQuizAnswerDto | null = null;\n  @bindable() public disabled = false;\n  @bindable() public selectedAnswers: number[] = [];\n  @bindable() public language: string | null = null;\n\n  @computedFrom('answers', 'language')\n  public get answerOptions(): IPossibleClassicAnswerOption[] {\n    if(!this.answers) return [];\n    return [\n      this.buildAnswer(1, this.getTranslation(this.answers).text1),\n      this.buildAnswer(2, this.getTranslation(this.answers).text2),\n      this.buildAnswer(3, this.getTranslation(this.answers).text3),\n      this.buildAnswer(4, this.getTranslation(this.answers).text4),\n    ];\n  }\n\n  private getTranslation(answers: IQuizAnswerDto): Pick<IQuizAnswerDto, Exclude<keyof IAnswerTranslationDto, 'language'>> {\n    return QuestionUtils.getTranslatedAnswer(answers, this.language);\n  }\n\n  private buildAnswer(\n    answerId: number,\n    text: string,\n  ): IPossibleClassicAnswerOption {\n    return {\n      answerId,\n      buttonId: `answer${answerId}`,\n      text,\n      dataTestSelector: `testexam-answer${answerId}`,\n    };\n  }\n\n  private isSelected(answerId: number, selectedAnswers: number[]): boolean {\n    return selectedAnswers.includes(answerId);\n  }\n}\n","import { PLATFORM, FrameworkConfiguration } from 'aurelia-framework';\n\nexport function configure(config: FrameworkConfiguration): void {\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/possible-answers/possible-answers'));\n}\n","import {\n  autoinject,\n  bindable,\n  computedFrom,\n  Container,\n  PLATFORM,\n  useView,\n} from 'aurelia-framework';\nimport { I18N } from 'aurelia-i18n';\nimport { IAnswerTranslationDto } from 'common/dto/course/content/answer.interface';\nimport { QuestionType } from 'common/dto/course/content/course.types';\nimport { IQuizAnswerDto } from 'common/dto/course/quiz/quiz.answer.interface';\nimport { QuestionTheme } from 'common/enum/question-theme.enum';\nimport { ContrastMode } from 'common/enum/quiz.attempt.status.enum';\nimport { VerticalDisplayMode, ResponsivenessMode } from 'common/enum/responsiveness-mode.enum';\nimport { QuestionUtils } from 'common/utils/QuestionUtils';\nimport { AbstractPluginComponent, AbstractPluginComponentPlugin } from '../AbstractPluginComponent';\nimport { AnswerLetter } from '../button-exam/button-exam';\n\ninterface IPossibleAnswerOption {\n  answerId: number;\n  buttonId: string;\n  text: string;\n  dataTestSelector: string;\n  pictureUrl?: string;\n  pictureBlurHash?: string;\n  audioUrl?: string;\n  isCorrect?: boolean;\n  letter: AnswerLetter;\n  isAudioAnswer: boolean;\n}\n\nexport abstract class BasePossibleAnswersPlugin extends AbstractPluginComponentPlugin {\n  /**\n   * possible translations\n   * tk: false\n   * tk: true\n   */\n  public abstract translate(translationKey: string, opts?: Parameters<I18N['tr']>[1]): string;\n}\n\nexport enum AnswerTilingMode {\n  Default = 'default',\n  OneByFourTexts = 'one-by-four-texts',\n}\n\n/**\n * css variables:\n * |name|description|\n * |----|-----------|\n * |--possible-answers-scale-factor|scaling factor passed through to buttons|\n */\n@autoinject()\n@useView(PLATFORM.moduleName('./possible-answers.html'))\nexport class PossibleAnswers extends AbstractPluginComponent<BasePossibleAnswersPlugin> {\n  @bindable() public answers: IQuizAnswerDto | null = null;\n  @bindable() public questionType: QuestionType | null = null;\n  @bindable() public theme: QuestionTheme = QuestionTheme.Exam;\n  @bindable() public contrastMode: ContrastMode = ContrastMode.High;\n  @bindable() public responsiveness = ResponsivenessMode.Auto;\n  @bindable() public disabled = false;\n  @bindable() public voiceOverEnabled = false;\n  @bindable() public picturesEnabled = false;\n  @bindable() public selectedAnswers: number[] = [];\n  @bindable() public revealAnswer = false;\n  @bindable() public readonly = false;\n  @bindable() public answerTilingMode = AnswerTilingMode.Default;\n  @bindable() public language: string | null = null;\n  @bindable() public verticalDisplayMode = VerticalDisplayMode.Scale;\n\n  private VerticalDisplayMode = VerticalDisplayMode;\n\n  constructor(container: Container) {\n    super(container, [BasePossibleAnswersPlugin]);\n  }\n\n  @computedFrom('questionType', 'answers', 'picturesEnabled', 'voiceOverEnabled', 'language')\n  public get answerOptions(): IPossibleAnswerOption[] {\n    if(!this.answers) return [];\n    if(this.questionType === QuestionType.TF) {\n      const textA = this.getTranslation(this.answers).text1 || this.translate('true');\n      const textB = this.getTranslation(this.answers).text2 || this.translate('false');\n      return [\n        this.buildAnswer(1, 'A', textA, undefined, undefined, undefined, undefined, this.answers.isCorrect1),\n        this.buildAnswer(2, 'B', textB, undefined, undefined, undefined, undefined, this.answers.isCorrect2),\n      ];\n    }\n    return [\n      /* eslint-disable max-len */\n      this.buildAnswer(1, 'A', this.getTranslation(this.answers).text1, this.answers.pictureUrl1, this.answers.pictureBlurHash1, this.answers.audioUrl1, this.getTranslation(this.answers).ttsAudioUrl1, this.answers.isCorrect1),\n      this.buildAnswer(2, 'B', this.getTranslation(this.answers).text2, this.answers.pictureUrl2, this.answers.pictureBlurHash2, this.answers.audioUrl2, this.getTranslation(this.answers).ttsAudioUrl2, this.answers.isCorrect2),\n      this.buildAnswer(3, 'C', this.getTranslation(this.answers).text3, this.answers.pictureUrl3, this.answers.pictureBlurHash3, this.answers.audioUrl3, this.getTranslation(this.answers).ttsAudioUrl3, this.answers.isCorrect3),\n      this.buildAnswer(4, 'D', this.getTranslation(this.answers).text4, this.answers.pictureUrl4, this.answers.pictureBlurHash4, this.answers.audioUrl4, this.getTranslation(this.answers).ttsAudioUrl4, this.answers.isCorrect4),\n      /* eslint-enable max-len */\n    ];\n  }\n\n  @computedFrom('questionType', 'answers')\n  private get isUppercase(): boolean {\n    return !!this.answers\n      && this.questionType === QuestionType.TF\n      && !this.getTranslation(this.answers).text1.length\n      && !this.getTranslation(this.answers).text2.length;\n  }\n\n  private getTranslation(answers: IQuizAnswerDto): Pick<IQuizAnswerDto, Exclude<keyof IAnswerTranslationDto, 'language'>> {\n    return QuestionUtils.getTranslatedAnswer(answers, this.language);\n  }\n\n  private buildAnswer(\n    answerId: number,\n    letter: AnswerLetter,\n    text: string,\n    pictureUrl: string | null | undefined,\n    pictureBlurHash: string | null | undefined,\n    audioUrl: string | null | undefined,\n    ttsAudioUrl: string | null | undefined,\n    isCorrect: boolean | undefined,\n  ): IPossibleAnswerOption {\n    return {\n      answerId,\n      buttonId: `answer${answerId}`,\n      text: audioUrl ? this.translate('answer', { letter }) : text,\n      dataTestSelector: `testexam-answer${answerId}`,\n      pictureUrl: (this.picturesEnabled && pictureUrl) || undefined,\n      pictureBlurHash: pictureBlurHash ?? undefined,\n      audioUrl: (audioUrl || (this.voiceOverEnabled && ttsAudioUrl)) || undefined,\n      isCorrect,\n      letter,\n      isAudioAnswer: !!audioUrl,\n    };\n  }\n\n  private translate(translationKey: string, opts?: Parameters<I18N['tr']>[1]): string {\n    let tk = translationKey;\n    this.usePlugins(plugin => {\n      tk = plugin.translate(translationKey, opts);\n    });\n    return tk;\n  }\n\n  private isSelected(answerId: number, selectedAnswers: number[]): boolean {\n    return selectedAnswers.includes(answerId);\n  }\n}\n","import { PLATFORM, FrameworkConfiguration } from 'aurelia-framework';\n\nexport function configure(config: FrameworkConfiguration): void {\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/progress-bar/progress-bar'));\n}\n","import { bindable, PLATFORM, useView } from 'aurelia-framework';\nimport { ThemeColorType } from 'common/aurelia-classes/SiteThemer';\nimport { MathUtils } from 'common/utils/MathUtils';\n\n@useView(PLATFORM.moduleName('./progress-bar.html'))\nexport class ProgressBar {\n\n  public static MAX_PROGRESS = 100;\n\n  /**\n   * progress, number between -100 and 100\n   * if the number is negative, the progress starts from the right\n   */\n  @bindable() public progress: number | null = null;\n  /**\n   * use a fixed background-color instead of color themed option\n   */\n  @bindable() public backgroundColor: string | null = null;\n  @bindable() public type: ThemeColorType = ThemeColorType.Primary;\n  /**\n   * enable the CSS transition for the progress bar, default: true\n   */\n  @bindable() public transition = true;\n\n  private getProgressStyle(progress: number): Record<string, any> {\n    const clampedProgress = MathUtils.clamp(Math.abs(progress), 0, 100);\n    return {\n      width: `${clampedProgress}%`,\n    };\n  }\n\n  private get style(): Record<string, unknown> {\n    return this.backgroundColor != null ? {\n      'background-color': this.backgroundColor,\n    } : { };\n  }\n}\n","import { PLATFORM, FrameworkConfiguration } from 'aurelia-framework';\n\nexport function configure(config: FrameworkConfiguration): void {\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/qr-code-scanner/qr-code-scanner'));\n}\n","import { BarcodeFormat, BrowserMultiFormatReader, DecodeHintType } from '@zxing/library';\nimport { PLATFORM, useView } from 'aurelia-framework';\nimport { AppUtils } from 'common/utils/AppUtils';\nimport { assertNotNullOrUndefined } from 'common/utils/assert';\nimport { DomUtils } from 'common/utils/DomUtils';\nimport { LogUtils } from 'common/utils/LogUtils';\n\n/**\n * A generic QR-Code Scanner component.\n * Includes camera setup, error handling and a reticle but no further logic.\n * \n * @event {QrCodeScanEvent} scan - fired whenever something is found. Feel free to handle any way\n */\n@useView(PLATFORM.moduleName('./qr-code-scanner.html'))\nexport class QrCodeScanner {\n  private static ENUMERATE_DEVICES_SUPPORTED = AppUtils.isMobileApp || navigator && navigator.mediaDevices && 'enumerateDevices' in navigator.mediaDevices;\n  private static TRANSLATED_ERROR_NAMES: string[] = [];\n  private static IGNORED_ERROR_NAMES: string[] = [\n    // Happens when no QR code is seen right now, which is, like, always\n    'NotFoundException',\n    'n',\n  ];\n\n  private LoadingState = LoadingState;\n\n  private element: HTMLElement;\n  private videoElement: HTMLVideoElement | null = null;\n  private codeReader = new BrowserMultiFormatReader(new Map([[DecodeHintType.POSSIBLE_FORMATS, [BarcodeFormat.QR_CODE]]]));\n\n  private loadingState = LoadingState.Loading;\n  private errorTk: string | null = null;\n  private overlayState = OverlayState.Good;\n\n  constructor(\n    element: Element,\n  ) {\n    this.element = element as HTMLElement;\n  }\n\n  protected attached(): void {\n    if(!QrCodeScanner.ENUMERATE_DEVICES_SUPPORTED) {\n      this.setErrorTk('mediaStreamApiNotSupported');\n      return;\n    }\n\n    this.start();\n  }\n\n  protected detached(): void {\n    this.codeReader.reset();\n  }\n\n  private async start(): Promise<void> {\n    if(AppUtils.isMobileApp) {\n      const { permissions } = window.cordova.plugins;\n      await new Promise(resolve => {\n        permissions.requestPermissions([permissions.CAMERA], resolve, resolve);\n      });\n    }\n\n    try {\n      assertNotNullOrUndefined(this.videoElement, 'videoElement element not ref\\'d');\n\n      this.videoElement.oncanplay = () => {\n        this.loadingState = LoadingState.Ready;\n      };\n\n      await this.codeReader.decodeFromVideoDevice(null, this.videoElement, (result, err) => {\n        if(err && !QrCodeScanner.IGNORED_ERROR_NAMES.includes(err.name)) {\n          if(QrCodeScanner.TRANSLATED_ERROR_NAMES.includes(err.name)) this.setErrorTk(err.name);\n          else this.setErrorTk('unknown');\n        } else {\n          this.setErrorTk(null);\n        }\n\n        if(result) {\n          let anyAccepted = false;\n          DomUtils.fireEvent<QrCodeScanEvent['detail']>(this.element, {\n            name: 'scan',\n            detail: {\n              text: result.getText(),\n              markAccepted: () => { anyAccepted = true; },\n            },\n          });\n          if(anyAccepted) {\n            this.overlayState = OverlayState.Good;\n          } else {\n            this.overlayState = OverlayState.Bad;\n            this.setErrorTk('unknownQrCode');\n          }\n        } else {\n          this.overlayState = OverlayState.Neutral;\n        }\n      });\n    } catch(e) {\n      if(e.name === 'NotFoundError') this.setErrorTk('noDevice');\n      else {\n        LogUtils.exception(e);\n        this.setErrorTk('unknown');\n      }\n      this.loadingState = LoadingState.Error;\n    }\n  }\n\n  private setErrorTk(name: string | null): void {\n    this.errorTk = name ? `common.qr-code-scanner.errors.${name}` : null;\n  }\n}\n\nexport type QrCodeScanEvent = CustomEvent<{\n  /**\n   * The text content of the QR-Code\n   */\n  text: string;\n\n  /**\n   * Mark the text as an accepted result.\n   * Must be called immediately when the Event is handled to have an effect\n   */\n  markAccepted: () => void;\n}>;\n\nenum LoadingState {\n  Loading = 'loading',\n  Ready = 'ready',\n  Error = 'error'\n}\n\nenum OverlayState {\n  Neutral = 'neutral',\n  Good = 'good',\n  Bad = 'bad'\n}\n","import { PLATFORM, FrameworkConfiguration } from 'aurelia-framework';\n\nexport function configure(config: FrameworkConfiguration): void {\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/quiz-result-question-answer/quiz-result-question-answer'));\n}\n","import { bindable, useView, PLATFORM, Container } from 'aurelia-framework';\nimport { AbstractPluginComponent, AbstractPluginComponentPlugin } from '../AbstractPluginComponent';\n\nexport abstract class BaseQuizResultQuestionAnswerPlugin extends AbstractPluginComponentPlugin {\n  /**\n   * possible translations\n   * tk: correct\n   * tk: wrong\n   * tk: correctSelected\n   * tk: wrongSelected\n   */\n  public processTranslation(translationKey: string): string {\n    return translationKey;\n  }\n}\n\n@useView(PLATFORM.moduleName('./quiz-result-question-answer.html'))\nexport class QuizResultQuestionAnswer extends AbstractPluginComponent<BaseQuizResultQuestionAnswerPlugin> {\n  @bindable() public answerSelected = false;\n  @bindable() public answerCorrect = false;\n  @bindable() public secondaryAnswerSelected?: boolean;\n  @bindable() public secondaryAnswerCorrect?: boolean;\n  @bindable() public label: string | null = null;\n  @bindable() public text: string | null = null;\n  @bindable() public pictureUrl: string | null = null;\n  private hasSecondaryAnswers = false;\n\n  constructor(container: Container) {\n    super(container, [BaseQuizResultQuestionAnswerPlugin]);\n  }\n\n  private attached(): void {\n    this.hasSecondaryAnswers = (this.secondaryAnswerSelected !== undefined && this.secondaryAnswerCorrect !== undefined);\n  }\n\n  private getResultIcon(isSelected: boolean, isCorrect: boolean): string {\n    if(isSelected && isCorrect) {\n      return 'System/Answer Selects/Correct - Selected';\n    }\n\n    if(isSelected) {\n      return 'System/Answer Selects/Wrong - Selected';\n    }\n\n    if(isCorrect) {\n      return 'System/Answer Selects/Correct';\n    }\n\n    return 'System/Answer Selects/Wrong';\n  }\n\n  private getResultIconHelpTk(isSelected: boolean, isCorrect: boolean): string {\n    if(isSelected && isCorrect) {\n      return this.processTranslation('correctSelected');\n    }\n\n    if(isSelected) {\n      return this.processTranslation('wrongSelected');\n    }\n\n    if(isCorrect) {\n      return this.processTranslation('correct');\n    }\n\n    return this.processTranslation('wrong');\n  }\n\n  private processTranslation(translationKey: string): string {\n    let tk = translationKey;\n    this.usePlugins(plugin => {\n      tk = plugin.processTranslation(translationKey);\n    });\n    return tk;\n  }\n}\n","import { PLATFORM, FrameworkConfiguration } from 'aurelia-framework';\n\nexport function configure(config: FrameworkConfiguration): void {\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/quiz-result-question-block/quiz-result-question-block'));\n}\n","import { autoinject, bindable, computedFrom, Container, PLATFORM, useView } from 'aurelia-framework';\nimport { Router } from 'aurelia-router';\nimport { ICourseContentDto } from 'common/dto/course/content/course.interface';\nimport { QuestionType } from 'common/dto/course/content/course.types';\nimport { ICoursePageDto } from 'common/dto/course/content/page.interface';\nimport { IQuizAnswerDto } from 'common/dto/course/quiz/quiz.answer.interface';\nimport { IQuizQuestionDto } from 'common/dto/course/quiz/quiz.question.interface';\nimport { CourseUtils } from 'common/utils/CourseUtils';\nimport { QuestionUtils } from 'common/utils/QuestionUtils';\nimport { AbstractPluginComponent, AbstractPluginComponentPlugin } from '../AbstractPluginComponent';\n\nexport abstract class BaseQuizResultQuestionBlockPlugin extends AbstractPluginComponentPlugin {\n  /**\n   * possible translations\n   * tk: question\n   * tk: jumpToPage\n   * tk: correct\n   * tk: halfCorrect\n   * tk: wrong\n   * tk: false\n   * tk: true\n   */\n  public processTranslation(translationKey: string): string {\n    return translationKey;\n  }\n\n  public abstract translate(fullTranslationKey: string): string;\n\n  public abstract jumpToPage(course: ICourseContentDto, page: ICoursePageDto): void;\n}\n\n@autoinject()\n@useView(PLATFORM.moduleName('./quiz-result-question-block.html'))\nexport class QuizResultQuestionBlock extends AbstractPluginComponent<BaseQuizResultQuestionBlockPlugin> {\n  @bindable() public question: IQuizQuestionDto | null = null;\n  @bindable() public secondaryAnswers: IQuizAnswerDto[] | null = null;\n  @bindable() public questionIndex: number | null = null;\n  @bindable() public course: ICourseContentDto | null = null;\n  @bindable() public blinkEnabled = true;\n\n  private QuestionType = QuestionType;\n\n  private router: Router;\n\n  constructor(container: Container, router: Router) {\n    super(container, [BaseQuizResultQuestionBlockPlugin]);\n    this.router = router;\n  }\n\n  @computedFrom('question')\n  private get fitbTexts(): string[] | null {\n    if(this.question !== null && this.question.type === QuestionType.LT) {\n      return QuestionUtils.parseFillInTheBlankQuestion(this.question.text);\n    }\n\n    return null;\n  }\n\n  @computedFrom('course', 'question')\n  private get page(): ICoursePageDto | null {\n    const { question } = this;\n    if(!this.course || !question) {\n      return null;\n    }\n\n    const courseQuestion = this.course.questions.find(q => q.id === question.questionId);\n    if(!courseQuestion) {\n      return null;\n    }\n\n    const page = this.course.pages.find(p => p.id === courseQuestion.pageId);\n    if(!page || CourseUtils.isPageOfTrainingChapter(this.course, page.id)) {\n      return null;\n    }\n\n    return page;\n  }\n\n  @computedFrom('course', 'question')\n  private get hasSubQuestion(): boolean {\n    const { question } = this;\n    if(!this.course || !question) {\n      return false;\n    }\n\n    return this.course.questions.some(q => q.parentQuestionId === question.questionId);\n  }\n\n  @computedFrom('question')\n  private get isSubQuestion(): boolean {\n    return this.question?.parentQuestionId != null;\n  }\n\n  @computedFrom('course')\n  private get showDifficultyAndPoints(): boolean {\n    return QuestionUtils.isFsExam(this.course?.examType ?? undefined);\n  }\n\n  @computedFrom('question')\n  private get isPracticeExam(): boolean {\n    // If the backend populated achievedPoints/maxPoints, it's a practice exam question\n    // This also works correctly for sub-quizzes (e.g., C95 Praxis module within C95 Standard)\n    return this.question?.achievedPoints != null && this.question?.maxPoints != null;\n  }\n\n  @computedFrom('question')\n  private get difficulty(): string {\n    return this.question ? QuestionUtils.getDifficulty(this.question) : 'easy';\n  }\n\n  private getAnswerTextOrTranslation(text: string, translationKey: string): string {\n    if(this.question?.type === QuestionType.TF && !text.length) {\n      let tk = translationKey;\n      this.usePlugins(plugin => {\n        tk = plugin.translate(translationKey);\n      });\n      return tk;\n    }\n    return text;\n  }\n\n  private processTranslation(translationKey: string): string {\n    let tk = translationKey;\n    this.usePlugins(plugin => {\n      tk = plugin.processTranslation(translationKey);\n    });\n    return tk;\n  }\n\n  private jumpToPage(): void {\n    this.usePlugins(plugin => {\n      if(this.course && this.page) {\n        // necessary because this component can be used outside of the course area\n        plugin.jumpToPage(this.course, this.page);\n      }\n    });\n  }\n\n  private getAnswerArray(answer: IQuizAnswerDto): number[] {\n    return QuestionUtils.getSelectedMcAnswersArrayFromAnswer(answer);\n  }\n}\n","import { PLATFORM, FrameworkConfiguration } from 'aurelia-framework';\n\nexport function configure(config: FrameworkConfiguration): void {\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/recent-trainings-list/recent-trainings-list'));\n}\n","import { autoinject, bindable, computedFrom, PLATFORM, useView } from 'aurelia-framework';\nimport { I18N } from 'aurelia-i18n';\nimport { ITrainingListDto } from 'common/dto/course/progress/course.progress.interface';\nimport { TrainingSessionType } from 'common/enum/quiz.enum';\nimport { AppUtils } from 'common/utils/AppUtils';\nimport { DateUtils } from 'common/utils/DateUtils';\nimport { DomUtils } from 'common/utils/DomUtils';\nimport { IListStandardItem, TextStyle } from '../list-standard/list-standard';\nimport { IconSize } from '../x-icon/x-icon';\n\nexport const RECENT_TRAININGS_LIST_LIMIT = 5;\n\n/**\n * @event { LoadAdditionalEvent } load-additional\n * @event { NavigateToResultEvent } navigate-to-result\n */\n@autoinject()\n@useView(PLATFORM.moduleName('./recent-trainings-list.html'))\nexport class RecentTrainingsList {\n  @bindable() public recentTrainingsList: ITrainingListDto[] = [];\n  @bindable() public recentTrainingsCount: number = 0;\n  @bindable() public centerTitle: boolean = false;\n  @bindable() public loading: boolean = false;\n  @bindable() public clickable: boolean = true;\n  private isFrp: boolean = AppUtils.isFrp;\n  private domElement: HTMLElement;\n  private recentTrainingsLimit: number = RECENT_TRAININGS_LIST_LIMIT;\n\n  constructor(\n    element: Element,\n    private i18N: I18N,\n  ) {\n    this.domElement = element as HTMLElement;\n  }\n\n  @computedFrom('recentTrainingsList', 'clickable', 'recentTrainingsCount')\n  private get listItems(): IListStandardItem[] {\n    return this.recentTrainingsList\n      .map((training, index) => {\n        const minutesTrained = training.durationInMinutes;\n        const minutesTrainedText = this.i18N.tr('common.recent-training-list.xMinutes', { count: minutesTrained });\n        const questionsCorrectText = this.i18N.tr('common.recent-training-list.xOfYCorrect', { questionsCorrect: training.numCorrectQuestions, count: training.numTotalQuestions });\n        const trainingType = `common.recent-training-list.${this.getTextTkFromTrainingSessionType(training.type)}`;\n        return {\n          icons: [{\n            name: 'System/Arrow - Right',\n            size: IconSize.normal,\n            type: 'custom',\n            ariaLabel: trainingType,\n          }],\n          click: this.clickable ? {\n            callback: () => this.handleNavigateToTrainingResultClick(training.quizSessionId),\n          } : undefined,\n          separatorAfter: index < this.recentTrainingsList.length - 1 || this.recentTrainingsList.length === this.recentTrainingsCount,\n          text: {\n            primary: training.startTime ? DateUtils.formatUnixTimeWithTimezone(training.startTime, 'LLL') : '-',\n            secondary: {\n              textTk: trainingType,\n              textStyle: TextStyle.Timeout,\n            },\n            tertiary: `${minutesTrainedText} · ${questionsCorrectText}`,\n          },\n        };\n      });\n  }\n\n  private handleNavigateToTrainingResultClick(quizSessionId: number): void {\n    DomUtils.fireEvent(this.domElement, {\n      name: 'navigate-to-result',\n      bubbles: false,\n      detail: {\n        quizSessionId,\n      },\n    });\n  }\n\n  private loadAdditional(): void {\n    this.recentTrainingsLimit += RECENT_TRAININGS_LIST_LIMIT;\n    DomUtils.fireEvent(this.domElement, {\n      name: 'load-additional',\n      bubbles: false,\n      detail: {\n        limit: this.recentTrainingsLimit,\n      },\n    });\n  }\n\n  private getTextTkFromTrainingSessionType(type: TrainingSessionType): string {\n    switch(type) {\n      case TrainingSessionType.Free:\n        return 'free';\n      case TrainingSessionType.Chat:\n        return 'chat';\n      case TrainingSessionType.Marked:\n        return 'marked';\n      case TrainingSessionType.SpacedRepetition:\n        return 'spacedRepetition';\n      case TrainingSessionType.Unknown:\n      default:\n        return 'unknown';\n    }\n  }\n}\n\nexport type LoadAdditionalEvent = CustomEvent<{ limit: number }>;\nexport type NavigateToResultEvent = CustomEvent<{ quizSessionId: number }>;\n","import { bindable, computedFrom, PLATFORM, useView } from 'aurelia-framework';\r\nimport { DomUtils } from 'common/utils/DomUtils';\r\n\r\nexport enum IasJumpLinkStyle {\r\n  LINK = 'link',\r\n  BUTTON = 'button',\r\n  BUTTON_CORRECT = 'button-correct',\r\n  BUTTON_WRONG = 'button-wrong',\r\n}\r\n\r\n@useView(PLATFORM.moduleName('./ias-jump-link.html'))\r\nexport class IasJumpLink {\r\n  @bindable() public jumpSlideOrdinal: number | string = 1;\r\n  @bindable() public type: IasJumpLinkStyle = IasJumpLinkStyle.BUTTON_CORRECT;\r\n  @bindable() public text = '';\r\n\r\n  constructor(private element: Element) {\r\n  }\r\n\r\n  private jump(): boolean {\r\n    DomUtils.fireEvent(this.element, {\r\n      name: 'markdown-jump-slide-link-click',\r\n      bubbles: true,\r\n      detail: {\r\n        jumpSlideOrdinal: Number(this.jumpSlideOrdinal),\r\n      },\r\n    });\r\n    return false;\r\n  }\r\n\r\n  @computedFrom('type')\r\n  private get classList(): string {\r\n    switch(this.type) {\r\n      case IasJumpLinkStyle.LINK: return 'ias-jump-link__link';\r\n      case IasJumpLinkStyle.BUTTON: return 'ias-jump-link__button';\r\n      case IasJumpLinkStyle.BUTTON_CORRECT: return 'ias-jump-link__button ias-jump-link__button--correct';\r\n      case IasJumpLinkStyle.BUTTON_WRONG: return 'ias-jump-link__button ias-jump-link__button--wrong';\r\n    }\r\n  }\r\n}\r\n","import { bindable, computedFrom, PLATFORM, useView } from 'aurelia-framework';\r\nimport { selectState } from 'common/aurelia-decorators/select-state';\r\nimport { ChatMessageActionService } from 'common/aurelia-services/ChatMessageActionService';\r\nimport { ICourseContentDto } from 'common/dto/course/content/course.interface';\r\nimport { ICoursePageDto } from 'common/dto/course/content/page.interface';\r\nimport { IQuestionDto } from 'common/dto/course/content/question.interface';\r\nimport { courseContentSelectorCreator } from 'common/store/selectors/course.context.selectors';\r\n\r\n@useView(PLATFORM.moduleName('./internal-link.html'))\r\nexport class InternalLink {\r\n  @bindable() public contextIdentifier = '';\r\n  @bindable() public type: 'page' | 'question' | null = null;\r\n  @bindable() public value: string | number | null = null;\r\n\r\n  constructor(private chatMessageActionService: ChatMessageActionService) {\r\n  }\r\n\r\n  @selectState(InternalLink, courseContentSelectorCreator, ['contextIdentifier']) private courseContent?: ICourseContentDto | null;\r\n\r\n  @computedFrom('courseContent', 'value', 'type')\r\n  public get linkTitle(): string | undefined {\r\n    let text: string | undefined;\r\n    if(this.type === 'question') {\r\n      const question = this.findQuestion();\r\n      if(!question) return;\r\n      text = question.text || String(question.id);\r\n    }\r\n    if(this.type === 'page') {\r\n      const page = this.findPage();\r\n      if(!page) return;\r\n      text = page.title || page.externalId || String(page.id);\r\n    }\r\n    return text;\r\n  }\r\n\r\n  protected handleLinkClick(): void {\r\n    if(!this.courseContent) return;\r\n    if(this.type === 'page') {\r\n      const page = this.findPage();\r\n      if(!page) return;\r\n      this.chatMessageActionService.handlePageLinkAction(this.courseContent, page);\r\n    }\r\n    if(this.type === 'question') {\r\n      const question = this.findQuestion();\r\n      if(!question) return;\r\n      this.chatMessageActionService.handleQuestionLinkAction(this.courseContent, question);\r\n    }\r\n  }\r\n\r\n  private findQuestion(): IQuestionDto | undefined {\r\n    return this.courseContent?.questions.find(q => q.id === Number(this.value));\r\n  }\r\n\r\n  private findPage(): ICoursePageDto | undefined {\r\n    return this.courseContent?.pages.find(q => q.id === Number(this.value));\r\n  }\r\n}\r\n","import { PLATFORM, FrameworkConfiguration } from 'aurelia-framework';\r\n\r\nexport function configure(config: FrameworkConfiguration): void {\r\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/rich-text/rich-text'));\r\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/rich-text/components/internal-link'));\r\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/rich-text/components/ias-jump-link'));\r\n}\r\n","import { Editor } from '@tiptap/core';\nimport { bindable, computedFrom, PLATFORM, useView } from 'aurelia-framework';\nimport { AnnotationControl, tryAnnotateMarkdown } from 'common/markdown/ShowdownAnnotationExtensions';\nimport { assertNotNullOrUndefined } from 'common/utils/assert';\nimport { AureliaViewFactory } from '../text-editor/nodes/AureliaViewFactory';\nimport { loadText, prepareRichTextExtensions } from '../text-editor/rich-text-utils';\n\nexport enum RichTextMode {\n  SLIDE = 'slide',\n  CONTENT = 'content',\n  SYSTEM_TEXT = 'system-text',\n}\n\n@useView(PLATFORM.moduleName('./rich-text.html'))\nexport class RichText {\n  @bindable() public contextIdentifier: string | null = null;\n  @bindable() public text = '';\n  @bindable() public mode = RichTextMode.SLIDE;\n  @bindable() public annotate = true;\n  @bindable() public placeholderTk = 'common.rich-text.placeholder';\n\n  private annotationControl: AnnotationControl | null = null;\n\n  private editor: Editor | null = null;\n  private editorRef: HTMLElement | null = null;\n  private plainText: string = '';\n\n  constructor(\n    private element: Element,\n    private aureliaViewFactory: AureliaViewFactory,\n  ) { }\n\n  protected attached(): void {\n    assertNotNullOrUndefined(this.editorRef, 'Cannot init text editor without HTML Element');\n    this.editor = new Editor({\n      editable: false,\n      element: this.editorRef,\n      extensions: [\n        ...prepareRichTextExtensions(this.aureliaViewFactory, this.contextIdentifier),\n      ],\n      content: loadText(this.text),\n      onCreate: () => {\n        setTimeout(() => this.updateAnnotations(), 0);\n      },\n    });\n    this.plainText = this.editor?.getText() ?? '';\n  }\n\n  protected detached(): void {\n    this.editor?.destroy();\n    this.editor = null;\n  }\n\n  private textChanged(): void {\n    this.editor?.commands.setContent(loadText(this.text));\n    this.plainText = this.editor?.getText() ?? '';\n    setTimeout(() => this.updateAnnotations(), 0);\n  }\n\n  private annotateChanged(): void {\n    setTimeout(() => this.updateAnnotations(), 0);\n  }\n\n  private updateAnnotations(): void {\n    this.annotationControl?.remove();\n    if(this.annotate) {\n      this.annotationControl = tryAnnotateMarkdown(this.element as HTMLElement, true);\n    }\n  }\n\n  /**\n   * This is for visually marking newly inserted text blocks, as soon as the editor is called once, a JSON is generated\n   * and it will count as non-empty even though nothing is visible.\n   */\n  @computedFrom('text', 'editor.isEmpty')\n  private get isEmpty(): boolean {\n    return this.text == null\n      || this.text === ''\n      || !!this.editor?.isEmpty;\n  }\n}\n","// TODO: imperial colors go here eventually:\nexport const actorColors = [\n  makeColor({\n    200: '#95F464',\n    medium: '#7ADD48',\n  }),\n  makeColor({\n    200: '#64F495',\n    medium: '#48DD7A',\n  }),\n  makeColor({\n    200: '#F49564',\n    medium: '#DD7A48',\n  }),\n  makeColor({\n    200: '#6495F4',\n    medium: '#487ADD',\n  }),\n  makeColor({\n    200: '#9564F4',\n    medium: '#7A48DD',\n  }),\n  makeColor({\n    200: '#F46495',\n    medium: '#DD487A',\n  }),\n] as const;\n\nexport const helmetColors: Partial<Record<string, IRisiColor>> = {\n  blue: makeColor({\n    400: '#85a3c2',\n    700: '#29527a',\n    900: '#14293d',\n    medium: '#336699',\n  }),\n  green: makeColor({\n    400: '#66ac66',\n    700: '#005d00',\n    900: '#004600',\n    medium: '#007400',\n  }),\n  red: makeColor({\n    400: '#e3666a',\n    700: '#a70005',\n    900: '#7d0004',\n    medium: '#d10006',\n  }),\n  white: makeColor({\n    400: '#ebebeb',\n    700: '#b1b1b1',\n    900: '#858585',\n    medium: '#dddddd',\n  }),\n  gray: makeColor({\n    400: '#8f8f8f',\n    700: '#363636',\n    900: '#292929',\n    medium: '#444444',\n  }),\n  lightblue: makeColor({\n    400: '#81eefb',\n    700: '#24b6c6',\n    900: '#1b8895',\n    medium: '#2de3f8',\n  }),\n};\n\nexport function helmetColorFromName(headgearName: string): string | null {\n  switch(headgearName) {\n    case 'Helm1_01':\n    case 'Helm1_02':\n    case 'Helm2_02':\n      return 'blue';\n\n    case 'Helm1_03':\n    case 'Helm1_04':\n    case 'Helmet3_01':\n    case 'Helmet3_03':\n      return 'red';\n\n    case 'Helm1_05':\n    case 'Helm2_04':\n    case 'Helm2_05':\n    case 'Helmet2_05':\n      return 'gray';\n\n    case 'Helm1_06':\n    case 'Helm2_03':\n    case 'Helm2_06':\n    case 'Helmet3_06':\n      return 'white';\n\n    case 'Helm2_01':\n    case 'Helmet3_04':\n      return 'lightblue';\n\n    case 'Helmet3_02':\n    case 'Helmet3_07':\n      return 'green';\n\n    default: return null;\n  }\n}\n\nexport function helmetTypeFromName(headgearName: string): string {\n  if(/Helm(et)2/.test(headgearName)) return 'helmet2';\n  if(/Helm(et)3/.test(headgearName)) return 'helmet3';\n  return 'helmet1';\n}\n\ninterface IRisiColorShades {\n  100: string;\n  200: string;\n  300: string;\n  400: string;\n  500: string;\n  600: string;\n  700: string;\n  800: string;\n  900: string;\n}\n\nexport interface IRisiColor extends IRisiColorShades {\n  light: string;\n  medium: string;\n  dark: string;\n}\n\nfunction makeColor(config: Partial<IRisiColorShades> & Pick<IRisiColor, 'medium'>): IRisiColor {\n  const shades: IRisiColorShades = {\n    100: config.medium,\n    200: config.medium,\n    300: config.medium,\n    400: config.medium,\n    500: config.medium,\n    600: config.medium,\n    700: config.medium,\n    800: config.medium,\n    900: config.medium,\n    ...config,\n  };\n\n  return {\n    ...shades,\n    medium: shades[600],\n    light: shades[200],\n    dark: shades[800],\n  };\n}\n","import { PLATFORM, FrameworkConfiguration } from 'aurelia-framework';\n\nexport function configure(config: FrameworkConfiguration): void {\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/risi-game/risi-game'));\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/risi-game/risi-controls-b/risi-controls-b'));\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/risi-game/risi-course-learn-page-header/risi-course-learn-page-header'));\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/risi-game/risi-graphic/risi-graphic'));\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/risi-game/risi-learn-page/risi-learn-page'));\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/risi-game/risi-learn-end-page-content/risi-learn-end-page-content'));\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/risi-game/risi-learn-end-card/risi-learn-end-card'));\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/risi-game/risi-map/risi-map'));\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/risi-game/risi-map-marker/risi-map-marker'));\n}\n","import { autoinject, bindable, PLATFORM, useView } from 'aurelia-framework';\nimport { SubscriptionManager, SubscriptionManagerService } from 'common/aurelia-services/SubscriptionManagerService';\nimport { assertNotNullOrUndefined } from 'common/utils/assert';\nimport { DomUtils } from 'common/utils/DomUtils';\nimport { imageNameToString } from '../../../risi-game/images';\nimport { PlayerMove } from '../../../risi-game/RisiMoves';\nimport { RisiControlsPlayClickedEvent, RisiControlsResetClickedEvent } from '../types';\n\n@autoinject()\n@useView(PLATFORM.moduleName('./risi-controls-b.html'))\nexport class RisiControlsB {\n  @bindable() public executingMove = false;\n  @bindable() public playerMoves: PlayerMove[] = [];\n  @bindable() public moveIcons: Record<PlayerMove, keyof typeof imageNameToString> | null = null;\n  @bindable() public undoAvailable = false;\n\n  private actionScrollWrapper: HTMLElement | null = null;\n  private element: HTMLElement;\n  private subscriptionManager: SubscriptionManager;\n\n  private chosenAction: PlayerMove | null = null;\n\n  // for devices that use a mouse\n  private cardWidth = 80;\n  private hoverMode = false;\n  private hoverModeOffsets: number[] = [];\n  private hoveredCardIndex = 0;\n  private Math = Math;\n\n  constructor(\n    element: Element,\n    subscriptionManagerService: SubscriptionManagerService,\n  ) {\n    this.element = element as HTMLElement;\n    this.subscriptionManager = subscriptionManagerService.createSubscriptionManager();\n  }\n\n  protected attached(): void {\n    setTimeout(() => this.scrollCardToCenter('forward'));\n    assertNotNullOrUndefined(this.actionScrollWrapper, 'forgot to ref actionScrollWrapper');\n    this.subscriptionManager.subscribeToElementResize(this.element, () => { this.hoverMode = false; });\n    this.subscriptionManager.subscribeToElementResize(this.actionScrollWrapper, this.updateHasScrollbar.bind(this));\n  }\n\n  protected detached(): void {\n    this.subscriptionManager.disposeSubscriptions();\n  }\n\n  public reset(): void {\n    this.chosenAction = null;\n  }\n\n  private async handleActionClicked(move: PlayerMove): Promise<void> {\n    if(this.executingMove) return;\n\n    this.chosenAction = move;\n\n    this.scrollCardIntoView(move);\n\n    DomUtils.fireEvent<RisiControlsPlayClickedEvent['detail']>(this.element, {\n      name: 'risi-controls-play-clicked',\n      bubbles: false,\n      detail: { move },\n    });\n  }\n\n  private handleResetClicked(): void {\n    if(this.executingMove) return;\n\n    DomUtils.fireEvent<RisiControlsResetClickedEvent['detail']>(this.element, {\n      name: 'risi-controls-reset-clicked',\n      bubbles: false,\n    });\n  }\n\n  private handleUndoClicked(): void {\n    if(this.executingMove) return;\n\n    DomUtils.fireEvent<RisiControlsResetClickedEvent['detail']>(this.element, {\n      name: 'risi-controls-undo-clicked',\n      bubbles: false,\n    });\n  }\n\n  private scrollCardToCenter(action: PlayerMove): void {\n    const cardElement = this.getCardElement(action);\n    if(!cardElement) return;\n\n    this.actionScrollWrapper?.scrollTo({\n      left: cardElement.offsetLeft - (this.actionScrollWrapper.clientWidth - cardElement.clientWidth) / 2,\n      behavior: 'smooth',\n    });\n  }\n\n  private scrollCardIntoView(action: PlayerMove): void {\n    this.getCardElement(action)?.scrollIntoView({ behavior: 'smooth' });\n  }\n\n  private getCardElement(action: PlayerMove): HTMLElement | null {\n    return this.actionScrollWrapper?.querySelector<HTMLElement>(`.risi-controls-b__action-card--action-${action}`) ?? null;\n  }\n\n  private updateHasScrollbar(): void {\n    if(!this.actionScrollWrapper || this.playerMoves.length < 2) return;\n\n    const { clientHeight, scrollHeight, clientWidth } = this.actionScrollWrapper;\n    this.hoverMode = this.hoverMode || clientHeight !== scrollHeight;\n\n    if(this.hoverMode) {\n      const margin = 8;\n      const spacePerCard = (clientWidth - 2 * margin - this.cardWidth) / (this.playerMoves.length - 1);\n      this.hoverModeOffsets = this.playerMoves.map((move, index) => {\n        return index * spacePerCard + margin;\n      });\n      const hoveredCardIndex = this.playerMoves.findIndex(m => m === 'forward');\n      if(hoveredCardIndex === -1) return;\n      this.hoveredCardIndex = hoveredCardIndex;\n    } else {\n      this.hoverModeOffsets = [];\n    }\n  }\n\n  private handleMouseMove(event: MouseEvent): void {\n    if(!this.actionScrollWrapper || this.hoverModeOffsets.length < 2) return;\n    const mousePosX = event.clientX - this.actionScrollWrapper.getBoundingClientRect().x;\n\n    const spacePerCard = this.hoverModeOffsets[1] - this.hoverModeOffsets[0];\n    const hoveredCardIndex = this.hoverModeOffsets.findIndex(x => x + this.cardWidth / 2 > mousePosX - spacePerCard / 2);\n    if(hoveredCardIndex === -1) return;\n    this.hoveredCardIndex = hoveredCardIndex;\n  }\n}\n","import { autoinject, bindable, PLATFORM, useView } from 'aurelia-framework';\nimport { selectState } from 'common/aurelia-decorators/select-state';\nimport { ICoursePageDto } from 'common/dto/course/content/page.interface';\nimport {\n  navigationInfoSelectorCreator as navigationInfo,\n  pageByChildIdSelectorCreator as pageByChildId,\n  pageByIdSelectorCreator as pageById,\n} from 'common/store/selectors/course.context.selectors';\nimport { ICourseNavigationInfo } from 'common/store/store.interface';\n\n@useView(PLATFORM.moduleName('./risi-course-learn-page-header.html'))\n@autoinject()\nexport class RisiCourseLearnPageHeader {\n  @bindable() public pageId: number | null = null;\n  @bindable() public contextIdentifier: string | null = null;\n  @bindable() public chapterEnd = false;\n\n  @selectState(RisiCourseLearnPageHeader, pageById, ['contextIdentifier', 'pageId']) private page?: ICoursePageDto | null;\n  @selectState(RisiCourseLearnPageHeader, pageByChildId, ['contextIdentifier', 'pageId']) private parentPage?: ICoursePageDto | null;\n  @selectState(RisiCourseLearnPageHeader, navigationInfo, ['contextIdentifier', 'pageId']) private navigationInfo?: ICourseNavigationInfo | null;\n\n  public activate(model: {\n    contextIdentifier: string | null;\n    pageId: number | null;\n    chapterEnd: boolean;\n  }): void {\n    this.contextIdentifier = model.contextIdentifier;\n    this.pageId = model.pageId;\n    this.chapterEnd = model.chapterEnd;\n  }\n}\n","import { autoinject, bindable, PLATFORM, useView } from 'aurelia-framework';\nimport { SubscriptionManager, SubscriptionManagerService } from 'common/aurelia-services/SubscriptionManagerService';\nimport { AnimationUtils } from 'common/utils/AnimationUtils';\nimport { ArrayUtils } from 'common/utils/ArrayUtils';\nimport { DomUtils } from 'common/utils/DomUtils';\nimport { FunctionUtils } from 'common/utils/FunctionUtils';\nimport seedrandom from 'seedrandom';\nimport { imageNameToString } from '../../risi-game/images';\nimport { RisiController } from '../../risi-game/RisiController';\nimport { baseConfig, levels } from '../../risi-game/RisiLevels';\nimport { PlayerMove } from '../../risi-game/RisiMoves';\nimport {\n  Direction,\n  ICoords,\n  IRisiActor,\n  IRisiActorMove,\n  IRisiActorUndoMove,\n  IRisiConfig,\n  Marking,\n  RelativeDirection,\n  RisiLevelStyle,\n  RisiStatus,\n  Sign,\n  StopLine,\n} from '../../risi-game/RisiTypes';\nimport { RisiUtils } from '../../risi-game/RisiUtils';\nimport {\n  actorColors,\n  helmetColors,\n  IRisiColor,\n} from './colors';\nimport { RisiControlsB } from './risi-controls-b/risi-controls-b';\nimport {\n  IActorSprite,\n  IGraphic,\n  ITile,\n  RisiControlsPlayClickedEvent,\n} from './types';\n\n/**\n * @event { RisiLevelFinishedEvent } risi-level-finished\n * @event { CustomEvent } risi-level-play-sound-effect\n */\n@autoinject()\n@useView(PLATFORM.moduleName('./risi-game.html'))\nexport class RisiGame {\n  private static BEARING_OFFSET = 20;\n  private static ANIMATION_DURATION_MS = 500;\n  private static UNDO_ANIMATION_DURATION_MS = 250;\n\n  @bindable() public levelName: string | null = null;\n  @bindable() public shirtColorLight: string | null = null;\n  @bindable() public shirtColorShade: string | null = null;\n  @bindable() public helmetColor: string | null = 'red';\n  @bindable() public helmetType = 'helmet1';\n\n  private element: HTMLElement;\n  private subscriptionManager: SubscriptionManager;\n  private controls: RisiControlsB | null = null;\n  private controlType: 'A' | 'B' = 'B';\n\n  private config: IRisiConfig = {\n    ...baseConfig,\n    playerTarget: RelativeDirection.Straight,\n    arms: {},\n    suggestedSolution: [],\n  };\n\n  private actorSprites: IActorSprite[] = [];\n  private decoSprites: IActorSprite[] = [];\n  private tiles: ITile[][] = [[], [], []];\n  private risi: RisiController | null = null;\n  private executingMove = false;\n  private fadeout = true;\n  private blink = false;\n  private shuffledColors = ArrayUtils.shuffle<IRisiColor>([...actorColors]);\n  private undoAvailable = false;\n  private dialogModel: DialogModel<unknown> | null = null;\n  private answerDialog: ((value: any) => void) | null = null;\n\n  private playerMoves: PlayerMove[] = [\n    'turn-left',\n    'bear-left',\n    'signal-left',\n    'look-left',\n    'wait',\n    'forward',\n    'look',\n    'signal-right',\n    'bear-right',\n    'turn-right',\n  ];\n  private moveIcons: Record<PlayerMove, keyof typeof imageNameToString> = {\n    'bear-left': imageNameToString.RisiGameUiActionsBearLeft,\n    'bear-right': imageNameToString.RisiGameUiActionsBearRight,\n    forward: imageNameToString.RisiGameUiActionsForward,\n    'look-left': imageNameToString.RisiGameUiActionsLookLeft,\n    look: imageNameToString.RisiGameUiActionsLook,\n    'signal-left': imageNameToString.RisiGameUiActionsSignalLeft,\n    'signal-right': imageNameToString.RisiGameUiActionsSignalRight,\n    'turn-left': imageNameToString.RisiGameUiActionsTurnLeft,\n    'turn-right': imageNameToString.RisiGameUiActionsTurnRight,\n    wait: imageNameToString.RisiGameUiActionsWait,\n  };\n\n  constructor(\n    element: Element,\n    subscriptionManagerService: SubscriptionManagerService,\n  ) {\n    this.element = element as HTMLElement;\n    this.subscriptionManager = subscriptionManagerService.createSubscriptionManager();\n  }\n\n  protected async bind(): Promise<void> {\n    // TODO: maybe throw a warning/error if the level can not be found, but for dev purposes this works better\n    this.config = (this.levelName && levels[this.levelName]) || levels.demo || this.config;\n    this.initialize();\n\n    this.refreshTileLayers();\n    this.refreshDecoSprites();\n    this.refreshActors();\n    this.subscriptionManager.subscribeToInterval(() => { this.blink = !this.blink; }, 500);\n  }\n\n  protected unbind(): void {\n    this.subscriptionManager.disposeSubscriptions();\n  }\n\n  protected attached(): void {\n    window.setTimeout(() => void this.setFadeout(false), 500);\n  }\n\n  protected detached(): void {\n    this.fadeout = true;\n  }\n\n  private initialize(): void {\n    if(!this.risi) {\n      this.risi = new RisiController();\n    }\n\n    this.risi.initialize(this.config);\n    this.undoAvailable = false;\n    this.controls?.reset();\n    this.refreshTileLayers();\n    this.refreshDecoSprites();\n    this.refreshActors();\n  }\n\n  private async handlePlayClicked(event: RisiControlsPlayClickedEvent): Promise<void> {\n    const chosenAction = event.detail.move;\n    if(!chosenAction || this.executingMove) return;\n\n    DomUtils.fireEvent<CustomEvent<void>>(this.element, {\n      name: 'risi-level-play-sound-effect',\n      detail: void 0,\n    });\n    const moveOutcome = await this.runPlayerMove(chosenAction);\n    this.undoAvailable = true;\n\n    if(moveOutcome === RisiStatus.Success) {\n      await this.showVictoryDialog();\n      await this.setFadeout(true);\n      DomUtils.fireEvent<RisiLevelFinishedEvent['detail']>(this.element, {\n        name: 'risi-level-finished',\n        detail: { success: true },\n      });\n    } else if(moveOutcome === RisiStatus.Fail) {\n      const failReason = this.risi?.getFailReason() ?? 'unknown';\n      const undoMode = this.controlType === 'B';\n      const response = undoMode\n        ? await this.showFailUndoDialog(failReason)\n        : await this.showFailDialog(failReason);\n\n      if(response === 'exit' || !this.risi) {\n        await this.setFadeout(true);\n        DomUtils.fireEvent<RisiLevelFinishedEvent['detail']>(this.element, {\n          name: 'risi-level-finished',\n          detail: { success: false },\n        });\n      } else if(undoMode) {\n        this.runUndo();\n      } else {\n        await this.setFadeout(true);\n        this.initialize();\n        await this.setFadeout(false);\n      }\n    }\n  }\n\n  private async handleUndoClicked(): Promise<void> {\n    if(this.executingMove) return;\n\n    await this.runUndo();\n  }\n\n  private async startAutoPlay(): Promise<void> {\n    if(this.executingMove || !await this.showAutoPlayDialog()) return;\n\n    this.executingMove = true;\n    await this.setFadeout(true);\n    this.initialize();\n    await this.setFadeout(false);\n    for(const move of this.config.suggestedSolution ?? []) {\n      await this.runPlayerMove(move);\n    }\n\n    await this.showAutoPlayFinishedDialog();\n    await this.setFadeout(true);\n    this.initialize();\n    await this.setFadeout(false);\n    this.executingMove = false;\n  }\n\n  private async resetLevel(): Promise<void> {\n    if(this.executingMove || !await this.showConfirmResetDialog()) return;\n\n    this.executingMove = true;\n    await this.setFadeout(true);\n    this.initialize();\n    await this.setFadeout(false);\n    this.executingMove = false;\n  }\n\n  private refreshActors(): void {\n    const getModifiers = (actor: IRisiActor): string[] => {\n      const modifiers: string[] = [];\n      if(actor.lookingLeft) modifiers.push('looking-left');\n      if(actor.type === 'player') {\n        if(actor.handExtendedLeft) modifiers.push('signaling-left');\n        if(actor.handExtendedRight) modifiers.push('signaling-right');\n        modifiers.push(`helmet-${this.helmetType}`);\n      } else {\n        if(actor.signalingLeft) modifiers.push('signaling-left');\n        if(actor.signalingRight) modifiers.push('signaling-right');\n      }\n      return modifiers;\n    };\n\n    const actors = this.risi?.getActors() ?? [];\n\n    const actorSprites: IActorSprite[] = actors.map((a, idx) => {\n      const color = this.shuffledColors[idx % this.shuffledColors.length];\n      return {\n        coords: a.coords,\n        graphic: {\n          // TODO: more complex strategy once we have more vehicle types\n          imageName: a.type === 'player' ? 'RisiGameActorsBikerBiker' : 'RisiGameActorsCarCar',\n          rotation: a.direction,\n          translation: {\n            x: (a.bearingLeft ? -1 : 1) * RisiGame.BEARING_OFFSET,\n            y: 0,\n          },\n          fillColor1: a.type === 'player' ? {\n            200: this.shirtColorLight ?? undefined,\n            600: this.shirtColorShade ?? undefined,\n          } : {\n            200: color.light,\n            600: color.medium,\n          },\n          fillColor2: a.type === 'player' && this.helmetColor ? {\n            400: helmetColors[this.helmetColor]?.[400],\n            600: helmetColors[this.helmetColor]?.[600],\n            700: helmetColors[this.helmetColor]?.[700],\n            900: helmetColors[this.helmetColor]?.[900],\n          } : undefined,\n        },\n        baseClass: a.type === 'player' ? 'risi-game__biker-graphic' : 'risi-game__car-graphic',\n        modifiers: getModifiers(a),\n      };\n    });\n\n    const spaces = ArrayUtils.flatten(this.risi?.getSpaceMap() ?? [[]]);\n    spaces.forEach(s => {\n      if(s.goal) {\n        // Shadow :P\n        actorSprites.push({\n          coords: s.coords,\n          graphic: {\n            imageName: 'RisiGameUiGoal',\n            rotation: s.street?.directions?.[0] ?? 0,\n            filter: 'brightness(0) opacity(0.2)',\n          },\n          baseClass: 'risi-game__goal-graphic',\n        });\n\n        actorSprites.push({\n          coords: RisiUtils.getCoordsAhead(s.coords, Direction.Up, 0.2),\n          graphic: {\n            imageName: 'RisiGameUiGoal',\n            rotation: s.street?.directions?.[0] ?? 0,\n            filter: 'opacity(0.8)',\n          },\n          baseClass: 'risi-game__goal-graphic',\n        });\n      }\n      if(!s.street?.sign || s.street.directions?.[0] == null) return;\n      const { sign } = s.street;\n      const direction = s.street.directions[0];\n      let subSignRotation = Direction.Up;\n      if(sign.direction === RelativeDirection.Left) subSignRotation = RisiUtils.turnRight(subSignRotation);\n      if(sign.type !== Sign.RightOfWay) subSignRotation = RisiUtils.turnAround(subSignRotation);\n\n      const signOffset = sign.offset ?? (sign.type === Sign.RightOfWay ? 1 : 0);\n      const inverted = signOffset < -1;\n      const upXorInverted = (direction === Direction.Up) !== inverted;\n\n      actorSprites.push({\n        coords: RisiUtils.getCoordsAhead(s.coords, RisiUtils.turnRight(direction), 1, signOffset),\n        graphic: {\n          imageName: upXorInverted ? 'RisiGameSignsSignFront' : 'RisiGameSignsSignBack',\n        },\n        baseClass: 'risi-game__sign-graphic',\n        modifiers: [\n          `direction-${(inverted ? RisiUtils.turnAround(direction) : direction).toString()}`,\n          `sign-${sign.type}`,\n          ...(sign.direction && [RelativeDirection.Left, RelativeDirection.Right].includes(sign.direction) ? [\n            'sub-sign-row-course',\n            `sub-sign-rotated-${subSignRotation}`,\n          ] : []),\n        ],\n      });\n    });\n\n    this.actorSprites = actorSprites;\n  }\n\n  private refreshTileLayers(): void {\n    const spaceMap = this.risi?.getSpaceMap() ?? [[]];\n    const tileLayers: ITile[][] = [[], [], []];\n    const levelStyle = this.config.levelStyle ?? RisiLevelStyle.Outskirts;\n\n    ArrayUtils.flatten(spaceMap).forEach(s => {\n      const { coords } = s;\n\n      const createGraphic = (imageName: IGraphic['imageName'], rotation: number): ITile => ({\n        graphic: { imageName, rotation },\n        coords,\n      });\n\n      const neighbors = RisiUtils.getCardinalNeighboringSpaces(spaceMap, coords);\n\n      if(s.street) {\n        const cornerDirection = RisiUtils.detectInnerCorner(neighbors, (s1, s2) => !!(s1 && s2 && !s1.street && !s2.street));\n\n        if(cornerDirection != null) {\n          let cornerGraphic: IGraphic['imageName'];\n          switch(levelStyle) {\n            case RisiLevelStyle.City: cornerGraphic = 'RisiGameTilesGroundStreetCurve'; break;\n            case RisiLevelStyle.Suburbs: cornerGraphic = 'RisiGameTilesGroundStreetCurvePavement'; break;\n            default: cornerGraphic = 'RisiGameTilesGroundStreetCurveOuterGrass';\n          }\n          tileLayers[0].push(createGraphic(cornerGraphic, cornerDirection));\n          tileLayers[2].push(createGraphic('RisiGameTilesGroundCurbCurve', cornerDirection));\n        } else {\n          tileLayers[0].push(createGraphic('RisiGameTilesGroundStreet', 0));\n        }\n\n        const direction = s.street.directions?.[0] ?? 0;\n\n        if(s.street.stopLine) {\n          switch(s.street.stopLine) {\n            case StopLine.Solid:\n              tileLayers[1].push(createGraphic('RisiGameTilesMarkingsSolidStraight', direction));\n              tileLayers[1].push(createGraphic('RisiGameTilesMarkingsStopline', direction));\n              break;\n            case StopLine.Dashed:\n              tileLayers[1].push(createGraphic('RisiGameTilesMarkingsSolidStraight', direction));\n              tileLayers[1].push(createGraphic('RisiGameTilesMarkingsStoplineDashed', direction));\n              break;\n            default:\n              tileLayers[1].push(createGraphic('RisiGameTilesMarkingsDashedStraight', direction));\n          }\n        } else {\n          let graphicName: IGraphic['imageName'] | undefined;\n\n          switch(s.street.marking) {\n            case Marking.Solid: graphicName = 'RisiGameTilesMarkingsSolidStraight'; break;\n            case Marking.Dashed: graphicName = 'RisiGameTilesMarkingsDashedStraight'; break;\n            case Marking.DashedCurve: graphicName = 'RisiGameTilesMarkingsDashedCurve'; break;\n            default:\n          }\n\n          if(graphicName) tileLayers[1].push(createGraphic(graphicName, direction));\n        }\n      } else {\n        const innerCornerDirection = RisiUtils.detectInnerCorner(neighbors, (s1, s2) => !!(s1?.street && s2?.street));\n        if(innerCornerDirection != null) {\n          let cornerGraphic: IGraphic['imageName'];\n          switch(levelStyle) {\n            case RisiLevelStyle.City: cornerGraphic = 'RisiGameTilesGroundPavementCurve'; break;\n            case RisiLevelStyle.Suburbs: cornerGraphic = 'RisiGameTilesGroundPavement2Curve'; break;\n            default: cornerGraphic = 'RisiGameTilesGroundGrassCurve';\n          }\n          tileLayers[0].push(createGraphic(cornerGraphic, innerCornerDirection));\n          tileLayers[2].push(createGraphic('RisiGameTilesGroundCurbCurve', innerCornerDirection));\n        } else {\n          const edgeDirection = RisiUtils.detectEdge(neighbors, neighbor => !!neighbor?.street);\n\n          if(edgeDirection != null) {\n            let edgeGraphic: IGraphic['imageName'] | undefined;\n            switch(levelStyle) {\n              case RisiLevelStyle.City: edgeGraphic = 'RisiGameTilesGroundPavement'; break;\n              case RisiLevelStyle.Suburbs: edgeGraphic = 'RisiGameTilesGroundPavement2'; break;\n              default:\n            }\n\n            if(edgeGraphic) {\n              tileLayers[0].push(createGraphic(edgeGraphic, 0));\n              tileLayers[2].push(createGraphic('RisiGameTilesGroundPavementBorder', edgeDirection));\n            }\n\n            const nextSpace = RisiUtils.getSpace(spaceMap, RisiUtils.getCoordsAhead(coords, edgeDirection, 1, 1));\n            const previousSpace = RisiUtils.getSpace(spaceMap, RisiUtils.getCoordsAhead(coords, edgeDirection, -1, 1));\n\n            if((!nextSpace || nextSpace.street) && (!previousSpace || previousSpace.street)) {\n              tileLayers[2].push(createGraphic('RisiGameTilesGroundCurb', RisiUtils.turnAround(edgeDirection)));\n            }\n          } else {\n            const diagonalNeighbors = RisiUtils.getDiagonalNeighboringSpaces(spaceMap, coords);\n            const outerCornerDirection = RisiUtils.detectOuterCorner(diagonalNeighbors, space => !!space?.street);\n\n            if(outerCornerDirection != null) {\n              let cornerGraphic: IGraphic['imageName'] | undefined;\n              switch(levelStyle) {\n                case RisiLevelStyle.City: cornerGraphic = 'RisiGameTilesGroundPavementCurveGrass'; break;\n                case RisiLevelStyle.Suburbs: cornerGraphic = 'RisiGameTilesGroundPavement2CurveGrass'; break;\n                default:\n              }\n\n              if(cornerGraphic) {\n                tileLayers[0].push(createGraphic(cornerGraphic, outerCornerDirection));\n                tileLayers[2].push(createGraphic('RisiGameTilesGroundPavementBorderCurve', outerCornerDirection));\n              }\n            }\n          }\n        }\n      }\n    });\n\n    this.tiles = tileLayers;\n  }\n\n  private refreshDecoSprites(): void {\n    const rng = seedrandom(`risi-level-${this.levelName}`);\n    const {\n      origin, levelStyle, rows, arms,\n    } = this.config;\n\n    function choose<T>(list: T[]): T {\n      return list[Math.floor(list.length * rng())];\n    }\n\n    function buildHouse(x: number, y: number, rotation: number): void {\n      decoSprites.push({\n        coords: { x, y },\n        graphic: {\n          imageName: levelStyle === RisiLevelStyle.City\n            ? choose<IGraphic['imageName']>([\n              'RisiGameBuildingsHouse01', 'RisiGameBuildingsHouse02', 'RisiGameBuildingsHouse03',\n              'RisiGameBuildingsHouse01', 'RisiGameBuildingsHouse02', 'RisiGameBuildingsHouse03',\n              'RisiGameBuildingsHouse01Grass', 'RisiGameBuildingsHouse02Grass', 'RisiGameBuildingsHouse03Grass',\n            ])\n            : choose<IGraphic['imageName']>([\n              'RisiGameBuildingsHouse01Grass', 'RisiGameBuildingsHouse02Grass', 'RisiGameBuildingsHouse03Grass',\n            ]),\n          scale: 2,\n          rotation,\n        },\n      });\n    }\n\n    function buildBush(x: number, y: number): void {\n      if(rng() > 0.2) {\n        decoSprites.push({\n          coords: { x: x - 0.5 + rng(), y: y - 0.5 + rng() },\n          graphic: {\n            imageName: choose<IGraphic['imageName']>([\n              'RisiGameFoliage01', 'RisiGameFoliage02', 'RisiGameFoliage03', 'RisiGameFoliage04',\n            ]),\n            scale: 0.5 + rng(),\n            rotation: 0,\n          },\n        });\n      }\n    }\n\n    function buildHouseOrBush(x: number, y: number, rotation: number): void {\n      if(rng() > 0.5) buildHouse(x, y, rotation);\n      else buildBush(x, y);\n    }\n\n    const decoSprites: IActorSprite[] = [];\n    switch(levelStyle) {\n      case RisiLevelStyle.City:\n        for(let i = origin.y - (arms.east ? 4 : 2); i > -1; i -= 2) buildHouse(origin.x + 2.5, i + 0.5, 180);\n        for(let i = origin.y + (arms.east ? 2 : 0); i < rows; i += 2) buildHouse(origin.x + 2.5, i + 0.5, 180);\n        for(let i = origin.y - (arms.west ? 4 : 2); i > -1; i -= 2) buildHouse(origin.x - 3.5, i + 0.5, 0);\n        for(let i = origin.y + (arms.west ? 2 : 0); i < rows; i += 2) buildHouse(origin.x - 3.5, i + 0.5, 0);\n        break;\n      case RisiLevelStyle.Suburbs:\n        for(let i = origin.y - (arms.east ? 4 : 2); i > -1; i -= 2) buildHouseOrBush(origin.x + 2.5, i + 0.5, 180);\n        for(let i = origin.y + (arms.east ? 2 : 0); i < rows; i += 2) buildHouseOrBush(origin.x + 2.5, i + 0.5, 180);\n        for(let i = origin.y - (arms.west ? 4 : 2); i > -1; i -= 2) buildHouseOrBush(origin.x - 3.5, i + 0.5, 0);\n        for(let i = origin.y + (arms.west ? 2 : 0); i < rows; i += 2) buildHouseOrBush(origin.x - 3.5, i + 0.5, 0);\n        break;\n      case RisiLevelStyle.Outskirts:\n        for(let i = origin.y - (arms.east ? 4 : 2); i > -1; i -= 2) buildBush(origin.x + 1.5, i + 0.5);\n        for(let i = origin.y + (arms.east ? 2 : 0); i < rows; i += 2) buildBush(origin.x + 1.5, i + 0.5);\n        for(let i = origin.y - (arms.west ? 4 : 2); i > -1; i -= 2) buildBush(origin.x - 2.5, i + 0.5);\n        for(let i = origin.y + (arms.west ? 2 : 0); i < rows; i += 2) buildBush(origin.x - 2.5, i + 0.5);\n        if(!arms.north) {\n          for(let i = origin.y - 4; i > -1; i -= 2) buildBush(origin.x - 0.5, i + 0.5);\n        }\n        break;\n      default:\n    }\n    this.decoSprites = decoSprites;\n  }\n\n  private getGridCoords(coords: ICoords): Record<string, number> {\n    return {\n      'grid-column': coords.x + 1,\n      'grid-row': coords.y + 1,\n    };\n  }\n\n  private getAbsoluteCoords(coords: ICoords): Record<string, string> {\n    return {\n      left: `${coords.x * 100 / this.config.columns}%`,\n      top: `${coords.y * 100 / this.config.rows}%`,\n      width: `${100 / this.config.columns}%`,\n      height: `${100 / this.config.rows}%`,\n    };\n  }\n\n  private getGridStyle(config: IRisiConfig): Record<string, number> {\n    return {\n      '--risi-game-columns': config.columns,\n      '--risi-game-rows': config.rows,\n    };\n  }\n\n  private getGraphicClass(baseClass?: string, modifiers?: string[]): string {\n    return [baseClass, ...(modifiers ?? []).map(m => `${baseClass}--${m}`)].join(' ');\n  }\n\n  private async runPlayerMove(type: PlayerMove): Promise<RisiStatus | undefined> {\n    this.executingMove = true;\n\n    const risiState = await this.risi?.execute({ type }, async moves => {\n      moves.forEach((m, idx) => this.updateSpriteModifiersForMove(m, this.actorSprites[idx]));\n      const animations = moves.map((m, idx) => this.getAnimationForMove(m, this.actorSprites[idx]));\n      await this.runAnimations(animations, RisiGame.ANIMATION_DURATION_MS);\n    });\n\n    this.refreshActors();\n\n    this.executingMove = false;\n\n    return risiState;\n  }\n\n  private async runUndo(): Promise<void> {\n    this.executingMove = true;\n\n    await this.risi?.undo(async moves => {\n      moves.forEach((m, idx) => this.updateSpriteModifiersForUndoMove(m, this.actorSprites[idx]));\n      const animations = moves.map((m, idx) => this.getAnimationForUndoMove(m, this.actorSprites[idx]));\n      await this.runAnimations(animations, RisiGame.UNDO_ANIMATION_DURATION_MS);\n    });\n\n    this.refreshActors();\n\n    this.executingMove = false;\n\n    this.undoAvailable = !!this.risi?.getUndoState()?.size;\n  }\n\n  private runAnimations(animations: Array<(progress: number) => void>, durationMs: number): Promise<void> {\n    return new Promise<void>(resolve => {\n      const animationFrameSubscription = AnimationUtils.setAnimationFrameInterval(elapsedMs => {\n        if(elapsedMs >= durationMs) {\n          animationFrameSubscription.cancel();\n          resolve();\n        } else {\n          const progress = elapsedMs / durationMs;\n          animations.forEach(a => a(progress));\n        }\n      });\n    });\n  }\n\n  // TODO: modularize/extract?\n  private updateSpriteModifiersForMove(move: IRisiActorMove, sprite: IActorSprite): void {\n    const spriteRef = sprite;\n    switch(move.type) {\n      case 'look-left': spriteRef.modifiers = [...sprite.modifiers ?? [], 'looking-left']; break;\n      case 'signal-left': spriteRef.modifiers = [...sprite.modifiers ?? [], 'signaling-left']; break;\n      case 'signal-right': spriteRef.modifiers = [...sprite.modifiers ?? [], 'signaling-right']; break;\n      default: break;\n    }\n  }\n\n  // TODO: modularize/extract?\n  private getAnimationForMove(move: IRisiActorMove, sprite: IActorSprite): (progress: number) => void {\n    switch(move.type) {\n      case 'forward':\n      case 'look-left':\n      case 'signal-left':\n      case 'signal-right': return this.animation(\n        () => this.getStraightAnimationData(move, sprite),\n        (data, progress) => this.updateSprite(data.sprite, {\n          coords: this.animateStraightMove(data.move.actor.coords, data.vector, progress),\n        }),\n      );\n\n      case 'wait': return this.animation(\n        () => ({ sprite }),\n        (data, progress) => this.animateLookLeftRight(progress, data.sprite),\n      );\n\n      case 'look': return this.animation(\n        () => this.getStraightAnimationData(move, sprite),\n        (data, progress) => {\n          this.updateSprite(data.sprite, {\n            coords: this.animateStraightMove(data.move.actor.coords, data.vector, progress),\n          });\n          this.animateLookLeftRight(progress, data.sprite);\n        },\n      );\n\n      case 'turn-left': return this.animation(() => ({\n        sprite,\n        move,\n        pivot: RisiUtils.getCoordsAhead(move.actor.coords, RisiUtils.turnLeft(move.actor.direction)),\n      }), (data, progress) => {\n        const rotation = data.move.actor.direction - progress * 90;\n        const rotationRadians = rotation / 180 * Math.PI;\n        this.updateSprite(data.sprite, {\n          coords: RisiUtils.addVectors(data.pivot, {\n            x: Math.cos(rotationRadians),\n            y: Math.sin(rotationRadians),\n          }),\n          graphic: this.setRotation(move.actor.bearingLeft ? this.animateBearing(data.sprite, 'right', progress ** 3) : sprite.graphic, rotation),\n        });\n      });\n\n      case 'turn-right': return this.animation(\n        () => this.getStraightAnimationData(move, sprite),\n        (data, progress) => this.updateSprite(data.sprite, {\n          coords: this.animateStraightMove(data.move.actor.coords, data.vector, progress),\n          graphic: this.setRotation(data.sprite.graphic, data.move.actor.direction + progress * 90),\n        }),\n      );\n\n      case 'bear-left': return this.animation(\n        () => this.getStraightAnimationData(move, sprite),\n        (data, progress) => this.updateSprite(data.sprite, {\n          graphic: !move.actor.bearingLeft ? this.animateBearing(data.sprite, 'left', progress) : sprite.graphic,\n          coords: this.animateStraightMove(data.move.actor.coords, data.vector, progress),\n        }),\n      );\n\n      case 'bear-right': return this.animation(\n        () => this.getStraightAnimationData(move, sprite),\n        (data, progress) => this.updateSprite(data.sprite, {\n          graphic: move.actor.bearingLeft ? this.animateBearing(data.sprite, 'right', progress) : sprite.graphic,\n          coords: this.animateStraightMove(data.move.actor.coords, data.vector, progress),\n        }),\n      );\n\n      default: return () => undefined;\n    }\n  }\n\n  private updateSpriteModifiersForUndoMove(undoMove: IRisiActorUndoMove, sprite: IActorSprite): void {\n    const spriteRef = sprite;\n    if(undoMove.oldState.lookingLeft) spriteRef.modifiers = [...sprite.modifiers ?? [], 'looking-left'];\n    if(undoMove.oldState.lookingRight) spriteRef.modifiers = [...sprite.modifiers ?? [], 'looking-right'];\n    if(undoMove.oldState.signalingLeft) spriteRef.modifiers = [...sprite.modifiers ?? [], 'signaling-left'];\n    if(undoMove.oldState.signalingRight) spriteRef.modifiers = [...sprite.modifiers ?? [], 'signaling-right'];\n  }\n\n  // TODO: modularize/extract?\n  private getAnimationForUndoMove(undoMove: IRisiActorUndoMove, sprite: IActorSprite): (progress: number) => void {\n    const { move, oldState } = undoMove;\n\n    switch(move.type) {\n      case 'forward':\n      case 'look-left':\n      case 'signal-left':\n      case 'signal-right': return this.animation(\n        () => this.getStraightAnimationData(move, sprite, RelativeDirection.Back),\n        (data, progress) => this.updateSprite(data.sprite, {\n          coords: this.animateStraightMove(data.move.actor.coords, data.vector, progress),\n        }),\n      );\n\n      case 'wait': return this.animation(\n        () => ({ sprite }),\n        (data, progress) => this.animateLookRightLeft(progress, data.sprite),\n      );\n\n      case 'look': return this.animation(\n        () => this.getStraightAnimationData(move, sprite, RelativeDirection.Back),\n        (data, progress) => {\n          this.updateSprite(data.sprite, {\n            coords: this.animateStraightMove(data.move.actor.coords, data.vector, progress),\n          });\n          this.animateLookRightLeft(progress, data.sprite);\n        },\n      );\n\n      case 'turn-left': return this.animation(() => ({\n        sprite,\n        move,\n        pivot: RisiUtils.getCoordsAhead(move.actor.coords, RisiUtils.turnLeft(move.actor.direction)),\n      }), (data, progress) => {\n        const rotation = data.move.actor.direction + progress * 90;\n        const rotationRadians = rotation / 180 * Math.PI;\n        this.updateSprite(data.sprite, {\n          coords: RisiUtils.addVectors(data.pivot, {\n            x: Math.cos(rotationRadians),\n            y: Math.sin(rotationRadians),\n          }),\n          graphic: this.setRotation(oldState.bearingLeft ? this.animateBearing(data.sprite, 'left', 1 - ((1 - progress) ** 3)) : sprite.graphic, rotation),\n        });\n      });\n\n      case 'turn-right': return this.animation(\n        () => this.getStraightAnimationData(move, sprite, RelativeDirection.Right),\n        (data, progress) => this.updateSprite(data.sprite, {\n          coords: this.animateStraightMove(data.move.actor.coords, data.vector, progress),\n          graphic: this.setRotation(data.sprite.graphic, data.move.actor.direction - progress * 90),\n        }),\n      );\n\n      case 'bear-left': return this.animation(\n        () => this.getStraightAnimationData(move, sprite, RelativeDirection.Back),\n        (data, progress) => this.updateSprite(data.sprite, {\n          graphic: move.actor.bearingLeft && !oldState.bearingLeft ? this.animateBearing(data.sprite, 'right', progress) : sprite.graphic,\n          coords: this.animateStraightMove(data.move.actor.coords, data.vector, progress),\n        }),\n      );\n\n      case 'bear-right': return this.animation(\n        () => this.getStraightAnimationData(move, sprite, RelativeDirection.Back),\n        (data, progress) => this.updateSprite(data.sprite, {\n          graphic: !move.actor.bearingLeft && oldState.bearingLeft ? this.animateBearing(data.sprite, 'left', progress) : sprite.graphic,\n          coords: this.animateStraightMove(data.move.actor.coords, data.vector, progress),\n        }),\n      );\n\n      default: return () => undefined;\n    }\n  }\n\n  private updateSprite(sprite: IActorSprite, update: Partial<IActorSprite>): void {\n    Object.assign(sprite, update);\n  }\n\n  private getStraightAnimationData(\n    move: IRisiActorMove,\n    sprite: IActorSprite,\n    direction = RelativeDirection.Straight,\n  ): { sprite: IActorSprite; move: IRisiActorMove; vector: ICoords } {\n    return {\n      sprite,\n      move,\n      vector: RisiUtils.getDirectionalVector(RisiUtils.turnBy(move.actor.direction, direction)),\n    };\n  }\n\n  private setRotation(graphic: IGraphic, rotation: number): IGraphic {\n    return {\n      ...graphic,\n      rotation,\n    };\n  }\n\n  private animateStraightMove(coords: ICoords, delta: ICoords, progress: number): ICoords {\n    return RisiUtils.addVectors(coords, RisiUtils.multiplyVector(delta, progress));\n  }\n\n  private animateBearing(sprite: IActorSprite, direction: 'left' | 'right', progress: number): IGraphic {\n    const factor = direction === 'left' ? -1 : 1;\n    return {\n      ...sprite.graphic,\n      translation: {\n        x: factor * (-RisiGame.BEARING_OFFSET + progress * 2 * RisiGame.BEARING_OFFSET),\n        y: 0,\n      },\n    };\n  }\n\n  private animateLookLeftRight(progress: number, sprite: IActorSprite): void {\n    const spriteRef = sprite;\n    if(!spriteRef.modifiers) spriteRef.modifiers = [];\n    if(progress < 0.5 && !spriteRef.modifiers.includes('looking-left')) {\n      spriteRef.modifiers = [...spriteRef.modifiers.filter(m => m !== 'looking-right'), 'looking-left'];\n    } else if(progress >= 0.5 && !spriteRef.modifiers.includes('looking-right')) {\n      spriteRef.modifiers = [...spriteRef.modifiers.filter(m => m !== 'looking-left'), 'looking-right'];\n    }\n  }\n\n  private animateLookRightLeft(progress: number, sprite: IActorSprite): void {\n    const spriteRef = sprite;\n    if(!spriteRef.modifiers) spriteRef.modifiers = [];\n    if(progress < 0.5 && !spriteRef.modifiers.includes('looking-right')) {\n      spriteRef.modifiers = [...spriteRef.modifiers.filter(m => m !== 'looking-left'), 'looking-right'];\n    } else if(progress >= 0.5 && !spriteRef.modifiers.includes('looking-left')) {\n      spriteRef.modifiers = [...spriteRef.modifiers.filter(m => m !== 'looking-right'), 'looking-left'];\n    }\n  }\n\n  private animation<TData>(setup: () => TData, update: (data: TData, progress: number) => void): (progress: number) => void {\n    const data = setup();\n    return progress => update(data, progress);\n  }\n\n  private async showDialog<T>(model: DialogModel<T>): Promise<T> {\n    return new Promise<T>(resolve => {\n      this.dialogModel = model;\n      this.answerDialog = (value: T) => {\n        this.dialogModel = null;\n        resolve(value);\n      };\n    });\n  }\n\n  private async showExitDialog(): Promise<void> {\n    const confirmed = await this.showDialog({\n      titleTk: 'common.risi-game.exitDialog.title',\n      descriptionTks: ['common.risi-game.exitDialog.text'],\n      buttons: [\n        {\n          value: true,\n          labelTk: 'common.risi-game.exitDialog.buttonConfirm',\n        },\n        {\n          value: false,\n          type: 'outlined',\n          labelTk: 'common.risi-game.exitDialog.buttonKeepPlaying',\n        },\n      ],\n    });\n    if(!confirmed) return;\n\n    DomUtils.fireEvent<RisiLevelFinishedEvent['detail']>(this.element, {\n      name: 'risi-level-finished',\n      detail: { success: false },\n    });\n  }\n\n  private async showVictoryDialog(): Promise<void> {\n    return this.showDialog({\n      titleTk: 'common.risi-game.successTitle',\n      descriptionTks: ['common.risi-game.successDescription'],\n      buttons: [{\n        value: void 0,\n        labelTk: 'common.risi-game.confirmSuccess',\n      }],\n    });\n  }\n\n  private async showFailDialog(reason: string): Promise<'retry' | 'exit'> {\n    return this.showDialog({\n      titleTk: 'common.risi-game.failureTitle',\n      descriptionTks: [\n        `common.risi-game.failReasons.${reason}`,\n        'common.risi-game.failureDescription',\n      ],\n      buttons: [\n        {\n          value: 'retry',\n          labelTk: 'common.risi-game.confirmFailureAndRetry',\n        },\n        {\n          value: 'exit',\n          type: 'outlined',\n          labelTk: 'common.risi-game.confirmFailureAndExit',\n        },\n      ],\n    });\n  }\n\n  private async showFailUndoDialog(reason: string): Promise<'retry' | 'exit'> {\n    return this.showDialog({\n      titleTk: 'common.risi-game.failureTitle',\n      descriptionTks: [\n        `common.risi-game.failReasons.${reason}`,\n        'common.risi-game.failureUndoDescription',\n      ],\n      buttons: [\n        {\n          value: 'retry',\n          iconName: 'Risi/UndoWhite',\n          labelTk: 'common.risi-game.confirmFailureAndUndo',\n        },\n        {\n          value: 'exit',\n          type: 'outlined',\n          labelTk: 'common.risi-game.confirmFailureAndExit',\n        },\n      ],\n    });\n  }\n\n  private async showAutoPlayDialog(): Promise<boolean> {\n    return this.showDialog({\n      titleTk: 'common.risi-game.startAutoPlayTitle',\n      descriptionTks: [\n        'common.risi-game.startAutoPlayDescription',\n        'common.risi-game.startAutoPlayDescription2',\n      ],\n      buttons: [\n        {\n          value: true,\n          labelTk: 'common.risi-game.confirmAutoPlay',\n        },\n        {\n          value: false,\n          type: 'outlined',\n          labelTk: 'general.quit',\n        },\n      ],\n    });\n  }\n\n  private async showAutoPlayFinishedDialog(): Promise<void> {\n    return this.showDialog({\n      titleTk: 'common.risi-game.autoPlayFinishedTitle',\n      descriptionTks: [\n        'common.risi-game.autoPlayFinishedDescription',\n        'common.risi-game.autoPlayFinishedDescription2',\n      ],\n      buttons: [\n        {\n          value: void 0,\n          labelTk: 'common.risi-game.confirmAutoPlayFinished',\n        },\n      ],\n    });\n  }\n\n  private async showConfirmResetDialog(): Promise<boolean> {\n    return this.showDialog({\n      titleTk: 'common.risi-game.resetTitle',\n      descriptionTks: ['common.risi-game.resetDescription'],\n      buttons: [\n        {\n          value: true,\n          labelTk: 'common.risi-game.confirmReset',\n        },\n        {\n          value: false,\n          type: 'outlined',\n          labelTk: 'general.quit',\n        },\n      ],\n    });\n  }\n\n  private async setFadeout(value: boolean): Promise<void> {\n    this.fadeout = value;\n    await FunctionUtils.timeout(value ? 1000 : 500);\n  }\n}\n\nexport type RisiLevelFinishedEvent = CustomEvent<{\n  success: boolean;\n}>;\n\nexport type DialogModel<T> = {\n  titleTk: string;\n  descriptionTks: string[];\n  buttons: Array<{\n    value: T;\n    iconName?: string;\n    type?: 'outlined';\n    labelTk: string;\n  }>;\n};","import { autoinject, bindable, PLATFORM, useView } from 'aurelia-framework';\nimport { SvgLoaderService } from 'common/aurelia-services/SvgLoaderService';\nimport { assertNotNullOrUndefined } from 'common/utils/assert';\nimport { imageNameToString } from '../../../risi-game/images';\nimport { IRisiColor } from '../colors';\nimport { IGraphic } from '../types';\n\n@autoinject()\n@useView(PLATFORM.moduleName('./risi-graphic.html'))\nexport class RisiGraphic {\n  @bindable() public graphic: IGraphic | null = null;\n\n  private element: HTMLElement;\n  private svgElement: SVGSVGElement | null = null;\n\n  constructor(\n    element: Element,\n    private svgLoaderService: SvgLoaderService,\n  ) {\n    this.element = element as HTMLElement;\n  }\n\n  private getStyle(graphic: IGraphic): Record<string, string> {\n    const style: Record<string, string> = {\n      transform: `scale(${graphic.scale ?? 1}) rotate(${graphic.rotation ?? 0}deg) translate(${graphic.translation?.x ?? 0}%, ${graphic.translation?.y ?? 0}%)`,\n    };\n\n    if(graphic.fillColor1) {\n      for(const sc of Object.keys(graphic.fillColor1)) {\n        const shade = graphic.fillColor1[sc as keyof IRisiColor];\n        if(shade) style[`--svg-fill-color-1-${sc}`] = shade;\n      }\n    }\n\n    if(graphic.fillColor2) {\n      for(const sc of Object.keys(graphic.fillColor2)) {\n        const shade = graphic.fillColor2[sc as keyof IRisiColor];\n        if(shade) style[`--svg-fill-color-2-${sc}`] = shade;\n      }\n    }\n\n    if(graphic.filter) style.filter = graphic.filter;\n\n    return style;\n  }\n\n  private async graphicChanged(newGraphic: IGraphic | null, oldGraphic?: IGraphic): Promise<void> {\n    const imageName = newGraphic?.imageName;\n    if(imageName === oldGraphic?.imageName) return;\n\n    if(imageName) {\n      const graphicUrl = imageNameToString[imageName];\n      assertNotNullOrUndefined(graphicUrl, `no url found for ${graphicUrl}`);\n      const svgElement = await this.svgLoaderService.getPrefixedSvgElement(graphicUrl);\n\n      // values could change while we wait for the icon to load, ignore the result if we loaded the wrong icon\n      if(imageName === this.graphic?.imageName) {\n        this.removeSvgElement();\n        this.svgElement = svgElement;\n        this.element.appendChild(svgElement);\n      }\n    } else {\n      this.removeSvgElement();\n    }\n  }\n\n  private removeSvgElement(): void {\n    this.svgElement?.remove();\n  }\n}\n","import { autoinject, bindable, PLATFORM, useView } from 'aurelia-framework';\nimport partyPopperAnimation from 'common/animations/party-popper.json';\nimport { LottieAnimationPlayer } from 'common/aurelia-components/lottie-animation-player/lottie-animation-player';\nimport { FunctionUtils } from 'common/utils/FunctionUtils';\n\n@autoinject()\n@useView(PLATFORM.moduleName('./risi-learn-end-card.html'))\nexport class RisiLearnEndCard {\n  @bindable() public newCoins = 0;\n  @bindable() public availableCoins = 0;\n  @bindable() public previousCoins = 0;\n  @bindable() public navPreview = false;\n\n  private element: HTMLElement;\n  private popperAnimationPlayerA: LottieAnimationPlayer | null = null;\n  private popperAnimationPlayerB: LottieAnimationPlayer | null = null;\n  private partyPopperAnimation = partyPopperAnimation;\n\n  /** runAnimation() remembers this when it starts, and cancels if this is different */\n  private currentAnimationHandle = 0;\n\n  constructor(element: Element) {\n    this.element = element as HTMLElement;\n  }\n\n  protected attached(): void {\n    this.runAnimations();\n  }\n\n  protected detached(): void {\n    this.currentAnimationHandle++;\n  }\n\n  private navPreviewChanged(): void {\n    this.runAnimations();\n  }\n\n  private async runAnimations(): Promise<void> {\n    this.resetAnimations();\n\n    if(this.navPreview || !this.element.isConnected) return;\n    const animationHandle = this.currentAnimationHandle;\n\n    await FunctionUtils.timeout(500);\n    if(this.currentAnimationHandle !== animationHandle) return;\n\n    // in case we got no new coins but we have already acquired all of them, just play the party poppers\n    this.popperAnimationPlayerA?.play();\n    this.popperAnimationPlayerB?.play();\n  }\n\n  private resetAnimations(): void {\n    this.currentAnimationHandle++;\n    if(this.navPreview || !this.element.isConnected) return;\n    this.popperAnimationPlayerA?.resetAnimation();\n    this.popperAnimationPlayerB?.resetAnimation();\n  }\n}\n","import { autoinject, bindable, PLATFORM, useView } from 'aurelia-framework';\nimport { ComposableCourseLearnPageHeader } from 'common/aurelia-components/course-learn-page-header/composable-course-learn-page-header';\n\n@useView(PLATFORM.moduleName('./risi-learn-end-page-content.html'))\n@autoinject()\nexport class RisiLearnEndPageContent {\n  @bindable() public pageId: number | null = null;\n  @bindable() public navPreview = false;\n  @bindable() public contextIdentifier: string | null = null;\n}\n","import { autoinject, bindable, ObserverLocator, Parser, PLATFORM, useView } from 'aurelia-framework';\nimport { CancellationToken } from 'common/aurelia-classes/CancellationToken';\nimport { CourseLearnFooter } from 'common/aurelia-components/course-learn-footer/course-learn-footer';\nimport { FocusReset } from 'common/aurelia-components/focus-reset/focus-reset';\nimport { SwipeNav, SwipingEvent } from 'common/aurelia-components/swipe-nav/swipe-nav';\nimport { selectState, SelectStateObservableComponent } from 'common/aurelia-decorators/select-state';\nimport { StoreService } from 'common/aurelia-services/StoreService';\nimport { SubscriptionManager, SubscriptionManagerService } from 'common/aurelia-services/SubscriptionManagerService';\nimport { LEARNING_COURSE_CONTEXT } from 'common/constants/content.editor.constants';\nimport { ICourseContentDto } from 'common/dto/course/content/course.interface';\nimport * as CourseContextMutations from 'common/store/mutations/course.context.mutations';\nimport { navigationInfoSelectorCreator } from 'common/store/selectors/course.context.selectors';\nimport { ICommonState, ICourseContextState, ICourseNavigationInfo } from 'common/store/store.interface';\nimport { CourseUtils as CommonCourseUtils } from 'common/utils/CourseUtils';\nimport { DomUtils } from 'common/utils/DomUtils';\nimport { RisiCourseLearnPageHeader } from '../risi-course-learn-page-header/risi-course-learn-page-header';\n\ntype PreparedPage = {\n  pageId: number | 'end-page' | null;\n  attachTo: 'previousSlot' | 'currentSlot' | 'nextSlot';\n};\n\n/**\n * @event { RisiLearnPageFinishedEvent } risi-learn-page-finished\n */\n@autoinject()\n@useView(PLATFORM.moduleName('./risi-learn-page.html'))\nexport class RisiLearnPage extends SelectStateObservableComponent {\n  public contextIdentifier = 'risi-game-content';\n  @bindable() public firstPageId: number | null = null;\n  @bindable() public pageId: number | null = null;\n  @bindable() public course: ICourseContentDto | null = null;\n\n  private subscriptionManager: SubscriptionManager;\n\n  @selectState(RisiLearnPage, navigationInfoSelectorCreator, ['contextIdentifier', 'pageId']) private navigationInfo?: ICourseNavigationInfo | null;\n\n  private usedAccessCodeId: number | null = null;\n  private previousPageId: number | null = null;\n  private nextPageId: number | null = null;\n  private parentPageId: number | null = null;\n  private rootPageId: number | null = null;\n  private nextRootChapterId: number | null = null;\n\n  private progress: number | null = 100;\n  private updateContextCancellationToken: CancellationToken | null = null;\n  private endPageShown = false;\n  private currentSwipeValue = 0;\n  private swipeNav: SwipeNav | null = null;\n  private element: HTMLElement;\n  private headerViewModel = RisiCourseLearnPageHeader;\n\n  /**\n   * This array always holds exactly three pages: previous, current and next (order doesn't matter, because they contain that information themselves)\n   * With this, the pages can be rendered and their references kept OUTSIDE of the swipe nav, and when a page changes, we can just move the new \"current page\" into the \"current slot\"\n   * by updating it. The other two elements, for the previous and next page, will be completely replaced in the array and therefore start fresh next time we see them\n   */\n  private preparedPages: [PreparedPage, PreparedPage, PreparedPage] | null = null;\n\n  constructor(\n    element: Element,\n    parser: Parser,\n    observerLocator: ObserverLocator,\n    private storeService: StoreService<ICommonState>,\n    subscriptionManagerService: SubscriptionManagerService,\n  ) {\n    super(parser, observerLocator);\n    this.element = element as HTMLElement;\n    this.subscriptionManager = subscriptionManagerService.createSubscriptionManager();\n  }\n\n  protected attached(): void {\n    this.pageId = this.firstPageId;\n    this.pageIdChanged();\n\n    this.updateViewportHeight();\n    // Don't ask why this has to be called twice in a row.\n    setTimeout(() => this.updateViewportHeight(), 0);\n    this.subscriptionManager.subscribeToResize(this.updateViewportHeight.bind(this));\n  }\n\n  protected detached(): void {\n    this.updateContextCancellationToken?.cancel();\n    this.updateContextCancellationToken = null;\n    this.storeService.disposeAllSubscriptions(this);\n    CourseLearnFooter.resetConfiguration(this);\n    this.subscriptionManager.disposeSubscriptions();\n  }\n\n  protected pageIdChanged(): void {\n    if(!this.element.isConnected) return;\n\n    this.updateContextCancellationToken?.cancel();\n    this.updateContextCancellationToken = null;\n    this.storeService.disposeAllSubscriptions(this);\n\n    const { pageId, course } = this;\n\n    if(!pageId || !course) return;\n\n    this.updateContextCancellationToken = new CancellationToken();\n    void this.updateCourseContextAndListenToChanges(course.id, pageId, this.updateContextCancellationToken);\n\n    CourseLearnFooter.configure(this, {\n      progress: this.progress ?? 0,\n      backButton: { callback: () => this.navigateBack() },\n      continueButton: { callback: () => this.navigateForward() },\n    });\n\n    const currentPage = course.pages.find(p => p.id === pageId);\n    this.previousPageId = (currentPage && CommonCourseUtils.findPreviousPage(course, currentPage, { allowTrainingOnly: true })?.id) || null;\n    this.parentPageId = currentPage?.parentId ?? null;\n    this.nextPageId = (currentPage && CommonCourseUtils.findNextPage(course, currentPage, { allowTrainingOnly: true })?.id) || null;\n    this.rootPageId = currentPage && CommonCourseUtils.getRootChapterForPageId(course, currentPage.id)?.id || null;\n    this.nextRootChapterId = currentPage && CommonCourseUtils.getNextRootChapterForPageId(course, currentPage.id, { allowTrainingOnly: true })?.id || null;\n    this.showCurrentPage();\n  }\n\n  private navigateBack(): void {\n    this.updateFooterToDefault();\n    FocusReset.reset();\n\n    if(this.isEndPageShown()) {\n      this.showCurrentPage();\n    } else {\n      const targetPageId = this.previousPageId;\n      if(targetPageId) {\n        this.pageId = targetPageId;\n      } else {\n        this.emitFinished(false);\n      }\n    }\n  }\n\n  private navigateForward(): void {\n    this.updateFooterToDefault();\n    FocusReset.reset();\n\n    if(this.nextPageId === null) {\n      if(!this.isEndPageShown()) {\n        this.showEndPage();\n      } else {\n        this.emitFinished(true);\n      }\n    } else {\n      const targetPageId = this.nextPageId;\n      if(targetPageId) {\n        this.pageId = targetPageId;\n      } else {\n        this.emitFinished(true);\n      }\n    }\n  }\n\n  private handleSwiping(event: SwipingEvent): void {\n    this.currentSwipeValue = event.detail.value;\n  }\n\n  private isEndPageShown(): boolean {\n    return this.isPageInSlot('currentSlot', 'end-page');\n  }\n\n  private isPageInSlot(slot: PreparedPage['attachTo'], value: PreparedPage['pageId']): boolean {\n    return this.preparedPages?.some(p => p.attachTo === slot && p.pageId === value) ?? false;\n  }\n\n  private showCurrentPage(): void {\n    this.setPreparedPages({\n      currentSlot: this.pageId,\n      previousSlot: this.previousPageId,\n      nextSlot: this.nextPageId ?? 'end-page',\n    });\n  }\n\n  private showEndPage(): void {\n    this.setPreparedPages({\n      currentSlot: 'end-page',\n      previousSlot: this.pageId,\n      nextSlot: null,\n    });\n    CourseLearnFooter.updateConfiguration(this, {\n      showOnMobileStacked: false,\n      backButton: {\n        text: {\n          tk: 'backToMap',\n        },\n        callback: () => this.emitFinished(true),\n        hide: true,\n      },\n      continueButton: {\n        hide: true,\n      },\n    });\n  }\n\n  private updateFooterToDefault(): void {\n    CourseLearnFooter.updateConfiguration(this, {\n      showOnMobileStacked: false,\n      progress: this.progress ?? 0,\n      backButton: {\n        callback: () => this.navigateBack(),\n      },\n      continueButton: { callback: () => this.navigateForward() },\n    });\n  }\n\n  /**\n   * Updates prepared pages, and tries to reuse the next \"currentPage\" from the previous state if it exists\n   * (for flickerless swipe (see description of 'preparedPages')\n   */\n  private setPreparedPages(pagesToSet: Record<PreparedPage['attachTo'], PreparedPage['pageId']>): void {\n    const existingPage = this.preparedPages?.find(p => p.pageId === pagesToSet.currentSlot);\n    if(existingPage) existingPage.attachTo = 'currentSlot';\n    this.preparedPages = [\n      existingPage ?? { attachTo: 'currentSlot', pageId: pagesToSet.currentSlot },\n      { attachTo: 'previousSlot', pageId: pagesToSet.previousSlot },\n      { attachTo: 'nextSlot', pageId: pagesToSet.nextSlot },\n    ];\n    this.endPageShown = pagesToSet.currentSlot === 'end-page';\n  }\n\n  /**\n   * Updates the course context, then listens to changes.\n   * If the route is deactivated before it gets there, remaining tasks get canceled\n   */\n  private async updateCourseContextAndListenToChanges(courseId: number, pageId: number, cancellationToken: CancellationToken): Promise<void> {\n    const context = (await this.storeService.getState()).contentContexts[this.contextIdentifier];\n    if(cancellationToken.canceled) return;\n    if(context == null || (context.state as ICourseContextState).courseId !== courseId) {\n      await this.storeService.dispatch(CourseContextMutations.setInitialState, this.contextIdentifier, courseId, pageId);\n    } else {\n      await this.storeService.dispatch(CourseContextMutations.setPageId, this.contextIdentifier, pageId);\n    }\n\n    if(cancellationToken.canceled) return;\n    this.storeService.subscribeToDetailedChanges(this, [\n      state => (state.contentContexts[LEARNING_COURSE_CONTEXT]?.state as ICourseContextState | undefined)?.pageId ?? null,\n      state => (state.contentContexts[LEARNING_COURSE_CONTEXT]?.state as ICourseContextState | undefined)?.courseId ?? null,\n    ], (_state, pId, cId) => this.handlePageIdChange(pId, cId));\n  }\n\n  private handlePageIdChange(pageId: number | null, courseId: number | null): void {\n    if(this.usedAccessCodeId == null) return;\n\n    if(pageId == null || courseId == null) {\n      this.emitFinished(false);\n    } else if(this.pageId !== pageId || this.course?.id !== courseId) {\n      this.pageId = pageId;\n    }\n  }\n\n  /**\n   * a bug in ios prevents us from using 100vh to set the page size\n   * to support safari on ios < 15.4 viewport height is calculated here\n   * ADDENDUM: here we calculate this based on the element, not the window.\n   */\n  private updateViewportHeight(): void {\n    const vh = this.element.clientHeight * 0.01;\n    this.element.style.setProperty('--course-learn-page-vh-in-px', `${vh}px`);\n  }\n\n  private emitFinished(success: boolean): void {\n    DomUtils.fireEvent<RisiLearnPageFinishedEvent['detail']>(this.element, {\n      name: 'risi-learn-page-finished',\n      detail: { success },\n    });\n  }\n}\n\nexport type RisiLearnPageFinishedEvent = CustomEvent<{\n  success: boolean;\n}>;\n","import { autoinject, computedFrom, PLATFORM } from 'aurelia-framework';\nimport { bindable, useView } from 'aurelia-templating';\nimport { helmetColors } from '../colors';\nimport { IGraphic } from '../types';\n\nexport enum RisiMapMarkerState {\n  FINISHED = 'finished',\n  CURRENT = 'current',\n  LOCKED = 'locked',\n}\n\n@autoinject\n@useView(PLATFORM.moduleName('./risi-map-marker.html'))\nexport class RisiMapMarker {\n  @bindable() public state = RisiMapMarkerState.LOCKED;\n  @bindable() public isCurrent = false;\n  @bindable() public isGame = false;\n\n  @bindable() public shirtColorLight: string | null = null;\n  @bindable() public shirtColorShade: string | null = null;\n\n  @bindable() public helmetType: string | null = null;\n  @bindable() public helmetColor: string | null = null;\n\n  protected CourseMapMarkerState = RisiMapMarkerState;\n\n  @computedFrom('shirtColorLight', 'shirtColorShade', 'helmetColor')\n  protected get avatarGraphic(): IGraphic {\n    return {\n      imageName: 'RisiGameActorsBikerBiker',\n      fillColor1: {\n        200: this.shirtColorLight ?? undefined,\n        600: this.shirtColorShade ?? undefined,\n      },\n      fillColor2: this.helmetColor ? {\n        400: helmetColors[this.helmetColor]?.[400],\n        600: helmetColors[this.helmetColor]?.[600],\n        700: helmetColors[this.helmetColor]?.[700],\n        900: helmetColors[this.helmetColor]?.[900],\n      } : undefined,\n    };\n  }\n\n  public checkGraphic: IGraphic = {\n    imageName: 'RisiGameUiFlag',\n  };\n}\n","import { autoinject, bindable, PLATFORM, useView } from 'aurelia-framework';\nimport { DomUtils } from 'common/utils/DomUtils';\nimport { FunctionUtils } from 'common/utils/FunctionUtils';\nimport { RisiMapMarkerState } from '../risi-map-marker/risi-map-marker';\nimport { IGraphic } from '../types';\n\n/**\n * @event { RisiMapIconClickedEvent } risi-map-icon-clicked\n */\n@autoinject\n@useView(PLATFORM.moduleName('./risi-map.html'))\nexport class RisiMap {\n  @bindable() public progress = 0;\n  @bindable() public shirtColorLight: string | null = null;\n  @bindable() public shirtColorShade: string | null = null;\n  @bindable() public helmetType = 'helmet1';\n  @bindable() public helmetColor: string | null = 'red';\n\n  private element: HTMLElement;\n  private mapObjectsGraphic: IGraphic = {\n    imageName: 'RisiGameMainmapObjectsPrerendered',\n  };\n\n  private chaptersInMap: Array<IChapterInfo> = [];\n  private chapterCircleElements: Array<Element> = [];\n  private currentChapterIndex: number | undefined;\n\n  private chapterData: Array<IChapterData> = [\n    { level: 'c-1', x: 16, y: 5.5 },\n    { level: '1-1', x: 48, y: 7.5 },\n    { level: 'c-2', x: 80.5, y: 9 },\n    { level: '1-2', x: 30.5, y: 13 },\n    { level: '2-1', x: 48, y: 19 },\n    { level: 'c-3', x: 25, y: 25.5 },\n    { level: '2-2', x: 65, y: 29.3 },\n    { level: '3-1', x: 65, y: 36.5 },\n    { level: '3-2', x: 65, y: 40.5 },\n    { level: 'c-4', x: 11, y: 40.5 },\n    { level: '3-3', x: 11, y: 46 },\n    { level: '3-4', x: 36, y: 49.3 },\n    { level: '4-1', x: 50, y: 53.5 },\n    { level: '4-2', x: 82, y: 53.5 },\n    { level: '4-3', x: 82, y: 60 },\n    { level: '4-4', x: 72.5, y: 64.5 },\n    { level: '4-5', x: 49, y: 68.8 },\n    { level: 'c-5', x: 48.3, y: 76.2 },\n    { level: '5-1', x: 82, y: 76.2 },\n    { level: '5-2', x: 82, y: 84 },\n    { level: '5-3', x: 57, y: 88 },\n    { level: '5-4', x: 41, y: 85.1 },\n    { level: '5-5', x: 17, y: 90 },\n  ];\n\n  constructor(\n    element: Element,\n  ) {\n    this.element = element as HTMLElement;\n  }\n\n  protected attached(): void {\n    this.updateChaptersInMap();\n  }\n\n  protected progressChanged(): void {\n    this.updateChaptersInMap();\n  }\n\n  protected handleChapterClick(chapter: IChapterInfo): void {\n    if(chapter.state === RisiMapMarkerState.LOCKED) return;\n    DomUtils.fireEvent<RisiMapIconClickedEvent['detail']>(this.element, {\n      name: 'risi-map-icon-clicked',\n      detail: {\n        type: this.isGame(chapter.level) ? 'level' : 'content',\n        id: chapter.level,\n        index: chapter.index,\n      },\n    });\n  }\n\n  private async updateChaptersInMap(): Promise<void> {\n    let gameNumber = 0;\n    let currentChapterIndex: number | undefined;\n    this.chaptersInMap = this.chapterData?.map<IChapterInfo>((data, index) => {\n      const isGame = this.isGame(data.level);\n      if(isGame) gameNumber++;\n\n      const progress = this.progress > index ? 1 : 0;\n      if(progress === 0 && currentChapterIndex == null) {\n        currentChapterIndex = index;\n      }\n\n      let state = RisiMapMarkerState.LOCKED;\n      if(currentChapterIndex === index) state = RisiMapMarkerState.CURRENT;\n      else if(currentChapterIndex == null) state = RisiMapMarkerState.FINISHED;\n\n      return {\n        level: data.level,\n        index,\n        coords: { x: data.x, y: data.y },\n        progress,\n        state,\n        isGame,\n        label: isGame ? `${gameNumber}` : '',\n      };\n    }) ?? [];\n\n    this.currentChapterIndex = currentChapterIndex ?? this.chaptersInMap.length - 1;\n\n    // Wait for chapterCircleElements refs to update\n    await FunctionUtils.timeout(0);\n    this.chapterCircleElements[this.currentChapterIndex]?.scrollIntoView({ block: 'center', inline: 'center' });\n  }\n\n  private isGame(level: string): boolean {\n    return level.match(/^\\d-\\d$/) != null;\n  }\n\n  private chapterIsCurrent(chapterState: RisiMapMarkerState, chapterIndex: number, progress: number): boolean {\n    return chapterState === 'current' || (chapterState === 'finished' && chapterIndex === this.chaptersInMap.length - 1 && progress >= this.chaptersInMap.length);\n  }\n}\n\ninterface IChapterInfo {\n  level: string;\n  index: number;\n  coords: IChapterMarkerCoords;\n  progress: number;\n  isGame: boolean;\n  state: RisiMapMarkerState;\n  label: string;\n}\n\ninterface IChapterData extends IChapterMarkerCoords {\n  level: string;\n}\n\ninterface IChapterMarkerCoords {\n  x: number;\n  y: number;\n}\n\nexport type RisiMapIconClickedEvent = CustomEvent<{\n  type: 'level' | 'content';\n  id: string;\n  index: number;\n}>;\n","import { PLATFORM, FrameworkConfiguration } from 'aurelia-framework';\n\nexport function configure(config: FrameworkConfiguration): void {\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/rounded-progress-bar/rounded-progress-bar'));\n}\n","import { autoinject, bindable, PLATFORM, useView } from 'aurelia-framework';\nimport { ThemeColorType } from 'common/aurelia-classes/SiteThemer';\nimport { LevelColor } from 'common/constants/levels.constants';\n\n@autoinject()\n@useView(PLATFORM.moduleName('./rounded-progress-bar.html'))\nexport class RoundedProgressBar {\n  @bindable public progress = 0;\n  @bindable public minProgress = 0;\n  @bindable public maxProgress = 100;\n  @bindable public progressColor: LevelColor | ThemeColorType = LevelColor.Bronze;\n  private domElement: HTMLElement;\n\n  constructor(\n    element: Element,\n  ) {\n    this.domElement = element as HTMLElement;\n  }\n\n  private progressChanged(): void {\n    this.updateProgressbar();\n  }\n\n  private updateProgressbar(): void {\n    this.domElement.style.setProperty('--rounded-progress-bar-percent', `${this.progress}%`);\n    if(this.progress === 0) {\n      this.domElement.style.removeProperty('--rounded-progress-bar-actual-progress-min-width');\n    } else {\n      this.domElement.style.setProperty('--rounded-progress-bar-actual-progress-min-width', '15px');\n    }\n  }\n}\n"],"names":[],"sourceRoot":""}