1 | import _intersection from "lodash/fp/intersection";
|
2 | import _findIndex from "lodash/fp/findIndex";
|
3 | import _includes from "lodash/fp/includes";
|
4 | import _shuffle from "lodash/fp/shuffle";
|
5 | import _isEmpty from "lodash/fp/isEmpty";
|
6 | import _isEqual from "lodash/fp/isEqual";
|
7 | import _sortBy from "lodash/fp/sortBy";
|
8 | import _filter from "lodash/fp/filter";
|
9 | import _last from "lodash/fp/last";
|
10 | import _slice from "lodash/fp/slice";
|
11 | import _size from "lodash/fp/size";
|
12 | import _head from "lodash/fp/head";
|
13 | import _pipe from "lodash/fp/pipe";
|
14 | import _find from "lodash/fp/find";
|
15 | import _get from "lodash/fp/get";
|
16 | import _map from "lodash/fp/map";
|
17 |
|
18 | function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
19 |
|
20 | import selectRule from '../rule-engine/select-rule';
|
21 | import updateVariables from '../rule-engine/apply-instructions';
|
22 | import updateState from '../update-state';
|
23 |
|
24 | const hasNoMoreLives = (config, state) => !config.livesDisabled && state.lives <= 0;
|
25 |
|
26 | const getContentRef = _get('content.ref');
|
27 |
|
28 | const hasRemainingLifeRequests = state => state.remainingLifeRequests > 0;
|
29 |
|
30 | const stepIsAlreadyExtraLife = state => getContentRef(state) === 'extraLife';
|
31 |
|
32 | const hasRulesToApply = chapterContent => {
|
33 | return !!(chapterContent && Array.isArray(chapterContent.rules) && !_isEmpty(chapterContent.rules));
|
34 | };
|
35 |
|
36 | export const nextSlidePool = (config, availableContent, state) => {
|
37 | if (state.nextContent.type === 'chapter') {
|
38 | const content = _find({
|
39 | ref: state.nextContent.ref
|
40 | }, availableContent) || null;
|
41 | return {
|
42 | currentChapterContent: content,
|
43 | nextChapterContent: content,
|
44 | temporaryNextContent: {
|
45 | type: 'slide',
|
46 | ref: ''
|
47 | }
|
48 | };
|
49 | }
|
50 |
|
51 | const lastSlideRef = _pipe(_get('slides'), _last)(state);
|
52 |
|
53 | const _currentIndex = _findIndex(({
|
54 | slides
|
55 | }) => !!_find({
|
56 | _id: lastSlideRef
|
57 | }, slides), availableContent);
|
58 |
|
59 | const currentIndex = _currentIndex !== -1 ? _currentIndex : 0;
|
60 | const currentChapterPool = availableContent[currentIndex] || null;
|
61 |
|
62 | const currentChapterSlideIds = _pipe(_get('slides'), _map('_id'))(currentChapterPool || []);
|
63 |
|
64 | const slidesAnsweredForThisChapter = _intersection(state.slides, currentChapterSlideIds);
|
65 |
|
66 | const isChapterCompleted = _size(slidesAnsweredForThisChapter) >= Math.min(config.slidesToComplete, _size(currentChapterSlideIds));
|
67 | const hasRules = hasRulesToApply(currentChapterPool);
|
68 | const shouldChangeChapter = !hasRules && isChapterCompleted;
|
69 |
|
70 | if (shouldChangeChapter) {
|
71 | const nextChapterContent = _pipe(_slice(currentIndex + 1, _size(availableContent)), _filter(content => !_isEmpty(content.slides)), _head)(availableContent);
|
72 |
|
73 | return {
|
74 | currentChapterContent: currentChapterPool,
|
75 | nextChapterContent,
|
76 | temporaryNextContent: {
|
77 | type: 'slide',
|
78 | ref: ''
|
79 | }
|
80 | };
|
81 | }
|
82 |
|
83 | return {
|
84 | currentChapterContent: currentChapterPool,
|
85 | nextChapterContent: currentChapterPool,
|
86 | temporaryNextContent: state.nextContent
|
87 | };
|
88 | };
|
89 |
|
90 | const _getChapterContent = (config, availableContent, state) => {
|
91 | const firstContent = _pipe(_filter(content => !_isEmpty(content.slides)), _head)(availableContent);
|
92 |
|
93 | if (!state) {
|
94 | return {
|
95 | currentChapterContent: firstContent,
|
96 | nextChapterContent: firstContent,
|
97 | temporaryNextContent: {
|
98 | type: 'slide',
|
99 | ref: ''
|
100 | }
|
101 | };
|
102 | }
|
103 |
|
104 | return nextSlidePool(config, availableContent, state);
|
105 | };
|
106 |
|
107 | const getChapterContent = (config, availableContent, state) => {
|
108 | const res = _getChapterContent(config, availableContent, state);
|
109 |
|
110 | if (!res.currentChapterContent) {
|
111 | return null;
|
112 | }
|
113 |
|
114 | return res;
|
115 | };
|
116 |
|
117 | const sortByPosition = _sortBy(slide => typeof slide.position === 'number' ? -slide.position : 0);
|
118 |
|
119 | const pickNextSlide = _pipe(_shuffle, sortByPosition, _head);
|
120 |
|
121 | const isTargetingIsCorrect = condition => condition.target.scope === 'slide' && condition.target.field === 'isCorrect';
|
122 |
|
123 | const getIsCorrect = (isCorrect, chapterRule) => {
|
124 | if (chapterRule.conditions.some(isTargetingIsCorrect)) return isCorrect;
|
125 | return null;
|
126 | };
|
127 |
|
128 | const computeNextSlide = (config, chapterContent, state) => {
|
129 | const remainingSlides = _filter(_pipe(_get('_id'), slideId => !state || !_includes(slideId, state.slides)), chapterContent.slides);
|
130 |
|
131 | return {
|
132 | type: 'slide',
|
133 | ref: pickNextSlide(remainingSlides)._id
|
134 | };
|
135 | };
|
136 |
|
137 | export const prepareStateToSwitchChapters = (chapterRule, state) => {
|
138 | if (!state) {
|
139 | return state;
|
140 | }
|
141 |
|
142 | return updateVariables(chapterRule.instructions)(_extends(_extends({}, state), {}, {
|
143 | nextContent: chapterRule.destination
|
144 | }));
|
145 | };
|
146 | export const computeNextStepForNewChapter = (config, state, chapterRule, isCorrect, availableContent) => {
|
147 |
|
148 | const nextStep = computeNextStep(config, prepareStateToSwitchChapters(chapterRule, state), availableContent, null);
|
149 |
|
150 | if (!nextStep) {
|
151 | return null;
|
152 | }
|
153 |
|
154 | return {
|
155 | nextContent: nextStep.nextContent,
|
156 | instructions: chapterRule.instructions.concat(nextStep.instructions || []),
|
157 | isCorrect: getIsCorrect(isCorrect, chapterRule)
|
158 | };
|
159 | };
|
160 |
|
161 | const extendPartialAction = (action, state) => {
|
162 | if (!action) {
|
163 | return null;
|
164 | }
|
165 |
|
166 | switch (action.type) {
|
167 | case 'answer':
|
168 | {
|
169 | const nextContent = action.payload.content || (state ? state.nextContent : {
|
170 | ref: '',
|
171 | type: 'node'
|
172 | });
|
173 | return {
|
174 | type: 'answer',
|
175 | payload: {
|
176 | answer: action.payload.answer,
|
177 | godMode: action.payload.godMode,
|
178 | isCorrect: action.payload.isCorrect,
|
179 | content: nextContent,
|
180 | nextContent,
|
181 | instructions: null
|
182 | }
|
183 | };
|
184 | }
|
185 |
|
186 | case 'extraLifeAccepted':
|
187 | {
|
188 | const nextContent = state ? state.nextContent : {
|
189 | ref: '',
|
190 | type: 'node'
|
191 | };
|
192 | return {
|
193 | type: 'extraLifeAccepted',
|
194 | payload: {
|
195 | content: nextContent,
|
196 | nextContent,
|
197 | instructions: null
|
198 | }
|
199 | };
|
200 | }
|
201 |
|
202 | default:
|
203 | return null;
|
204 | }
|
205 | };
|
206 |
|
207 | const computeNextStep = (config, _state, availableContent, partialAction) => {
|
208 | const action = extendPartialAction(partialAction, _state);
|
209 | const isCorrect = !!action && action.type === 'answer' && !!action.payload.isCorrect;
|
210 | const answer = !!action && action.type === 'answer' && action.payload.answer || [];
|
211 | const state = !_state || !action ? _state : updateState(config, _state, [action]);
|
212 | const chapterContent = getChapterContent(config, availableContent, state);
|
213 |
|
214 | if (!chapterContent) {
|
215 | return null;
|
216 | }
|
217 |
|
218 | const {
|
219 | currentChapterContent,
|
220 | nextChapterContent,
|
221 | temporaryNextContent
|
222 | } = chapterContent;
|
223 | const hasRules = hasRulesToApply(nextChapterContent);
|
224 |
|
225 | if (!hasRules) {
|
226 | if (state && hasNoMoreLives(config, state)) {
|
227 | return {
|
228 | nextContent: !stepIsAlreadyExtraLife(state) && hasRemainingLifeRequests(state) ? {
|
229 | type: 'node',
|
230 | ref: 'extraLife'
|
231 | } : {
|
232 | type: 'failure',
|
233 | ref: 'failExitNode'
|
234 | },
|
235 | instructions: null,
|
236 | isCorrect
|
237 | };
|
238 | } else if (!nextChapterContent) {
|
239 |
|
240 | return {
|
241 | nextContent: {
|
242 | type: 'success',
|
243 | ref: 'successExitNode'
|
244 | },
|
245 | instructions: null,
|
246 | isCorrect
|
247 | };
|
248 | }
|
249 | }
|
250 |
|
251 | if (hasRules) {
|
252 | const allAnswers = !!state && state.allAnswers || [];
|
253 |
|
254 | const chapterRule = selectRule(nextChapterContent.rules, _extends(_extends({}, state), {}, {
|
255 | nextContent: temporaryNextContent,
|
256 | allAnswers: [...allAnswers, {
|
257 | slideRef: temporaryNextContent.ref,
|
258 | answer,
|
259 | isCorrect
|
260 | }]
|
261 | }));
|
262 |
|
263 | if (!chapterRule) {
|
264 | return null;
|
265 | }
|
266 |
|
267 | if (chapterRule.destination.type === 'chapter') {
|
268 | return computeNextStepForNewChapter(config, state, chapterRule, isCorrect, availableContent);
|
269 | }
|
270 |
|
271 | return {
|
272 | nextContent: chapterRule.destination,
|
273 | instructions: chapterRule.instructions,
|
274 | isCorrect: _isEqual(currentChapterContent, nextChapterContent) ? getIsCorrect(isCorrect, chapterRule) : isCorrect
|
275 | };
|
276 | }
|
277 |
|
278 | if (nextChapterContent && Array.isArray(nextChapterContent.slides) && nextChapterContent.slides.length > 0) {
|
279 | const stateWithDecrementedLives = state ? _extends(_extends({}, state), {}, {
|
280 | nextContent: temporaryNextContent
|
281 | }) : state;
|
282 | const nextContent = computeNextSlide(config, nextChapterContent, stateWithDecrementedLives);
|
283 | return {
|
284 | nextContent,
|
285 | instructions: null,
|
286 | isCorrect
|
287 | };
|
288 | }
|
289 |
|
290 | return null;
|
291 | };
|
292 |
|
293 | export default computeNextStep;
|
294 |
|
\ | No newline at end of file |