UNPKG

9.06 kBJavaScriptView Raw
1import _intersection from "lodash/fp/intersection";
2import _findIndex from "lodash/fp/findIndex";
3import _includes from "lodash/fp/includes";
4import _shuffle from "lodash/fp/shuffle";
5import _isEmpty from "lodash/fp/isEmpty";
6import _isEqual from "lodash/fp/isEqual";
7import _sortBy from "lodash/fp/sortBy";
8import _filter from "lodash/fp/filter";
9import _last from "lodash/fp/last";
10import _slice from "lodash/fp/slice";
11import _size from "lodash/fp/size";
12import _head from "lodash/fp/head";
13import _pipe from "lodash/fp/pipe";
14import _find from "lodash/fp/find";
15import _get from "lodash/fp/get";
16import _map from "lodash/fp/map";
17
18function _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
20import selectRule from '../rule-engine/select-rule';
21import updateVariables from '../rule-engine/apply-instructions';
22import updateState from '../update-state';
23
24const hasNoMoreLives = (config, state) => !config.livesDisabled && state.lives <= 0;
25
26const getContentRef = _get('content.ref');
27
28const hasRemainingLifeRequests = state => state.remainingLifeRequests > 0;
29
30const stepIsAlreadyExtraLife = state => getContentRef(state) === 'extraLife';
31
32const hasRulesToApply = chapterContent => {
33 return !!(chapterContent && Array.isArray(chapterContent.rules) && !_isEmpty(chapterContent.rules));
34};
35
36export 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
90const _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
107const 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
117const sortByPosition = _sortBy(slide => typeof slide.position === 'number' ? -slide.position : 0);
118
119const pickNextSlide = _pipe(_shuffle, sortByPosition, _head);
120
121const isTargetingIsCorrect = condition => condition.target.scope === 'slide' && condition.target.field === 'isCorrect';
122
123const getIsCorrect = (isCorrect, chapterRule) => {
124 if (chapterRule.conditions.some(isTargetingIsCorrect)) return isCorrect;
125 return null;
126};
127
128const 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
137export 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};
146export const computeNextStepForNewChapter = (config, state, chapterRule, isCorrect, availableContent) => {
147 // eslint-disable-next-line no-use-before-define
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
161const 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
207const 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 // If user has answered all questions, return success endpoint
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 || []; // $FlowFixMe nextChapterContent.rules = array not emtpy -> checked by hasRulesToApply
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
293export default computeNextStep;
294//# sourceMappingURL=compute-next-step.js.map
\No newline at end of file