guess-the-year-web-component 3.2.3 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/custom-elements.json +19 -1
- package/dist/guess-the-year.d.ts +21 -16
- package/dist/guess-the-year.d.ts.map +1 -1
- package/dist/index.html +3 -8
- package/dist/lib/controller/scorecontroller.d.ts +2 -1
- package/dist/lib/controller/scorecontroller.d.ts.map +1 -1
- package/dist/lib/i18n/EN.d.ts.map +1 -1
- package/dist/lib/i18n/NL.d.ts.map +1 -1
- package/dist/main.bundle.js +200 -0
- package/dist/main.bundle.js.map +1 -0
- package/package.json +26 -27
- package/src/guess-the-year.ts +335 -273
- package/src/index.html +2 -7
- package/src/lib/ApiService.ts +1 -1
- package/src/lib/Util.ts +1 -1
- package/src/lib/controller/scorecontroller.ts +12 -1
- package/src/lib/i18n/EN.ts +18 -4
- package/src/lib/i18n/NL.ts +19 -5
- package/src/scss/_layout.scss +18 -5
- package/src/scss/_typography.scss +3 -2
- package/src/scss/_variables.scss +5 -5
- package/src/scss/main.scss +55 -1
- package/src/scss/views/_intro.scss +16 -4
- package/src/scss/views/_stage.scss +149 -43
- package/webpack.config.cjs +23 -25
- package/dist/guess-the-year.js +0 -751
- package/dist/guess-the-year.js.map +0 -1
- package/dist/lib/ApiService.js +0 -134
- package/dist/lib/ApiService.js.map +0 -1
- package/dist/lib/Icon.js +0 -2
- package/dist/lib/Icon.js.map +0 -1
- package/dist/lib/Incident.js +0 -37
- package/dist/lib/Incident.js.map +0 -1
- package/dist/lib/Util.js +0 -69
- package/dist/lib/Util.js.map +0 -1
- package/dist/lib/controller/scorecontroller.js +0 -38
- package/dist/lib/controller/scorecontroller.js.map +0 -1
- package/dist/lib/enums.js +0 -10
- package/dist/lib/enums.js.map +0 -1
- package/dist/lib/i18n/EN.js +0 -148
- package/dist/lib/i18n/EN.js.map +0 -1
- package/dist/lib/i18n/NL.js +0 -152
- package/dist/lib/i18n/NL.js.map +0 -1
- package/dist/lib/i18n/i18n.js +0 -10
- package/dist/lib/i18n/i18n.js.map +0 -1
- package/dist/main.js +0 -205
- package/todo.txt +0 -0
- /package/dist/{main.js.LICENSE.txt → main.bundle.js.LICENSE.txt} +0 -0
package/src/guess-the-year.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { customElement, property, state } from "lit/decorators.js";
|
|
|
3
3
|
import dayjs from "dayjs";
|
|
4
4
|
import { Dayjs } from "dayjs";
|
|
5
5
|
const { setTimeout } = window;
|
|
6
|
-
import { Incident
|
|
6
|
+
import { Incident } from "./lib/Incident";
|
|
7
7
|
import { NoImageImg } from "./lib/Icon";
|
|
8
8
|
import { ApiService, ContentFeedback } from "./lib/ApiService";
|
|
9
9
|
import { Util } from "./lib/Util";
|
|
@@ -14,7 +14,6 @@ import { QuestionScoreController } from "./lib/controller/scorecontroller";
|
|
|
14
14
|
|
|
15
15
|
const SPLASH_DELAY: number = 1000;
|
|
16
16
|
const MAX_QUESTIONS: number = 10;
|
|
17
|
-
const HINT_COST: number = 2;
|
|
18
17
|
const MIN_YEAR: number = 1950;
|
|
19
18
|
const MAX_YEAR: number = parseInt(dayjs().format('YYYY'), 10);
|
|
20
19
|
const GTY_FROM_YEAR_VAR = 'gty-from-year';
|
|
@@ -23,6 +22,8 @@ const GTY_NUM_QUESTIONS_VAR = 'gty-number-of-questions';
|
|
|
23
22
|
const MAX_DOB: number = 1915;
|
|
24
23
|
const MIN_DOB: number = parseInt(dayjs().format('YYYY'), 10) - 10;
|
|
25
24
|
const GTY_PLAYER_DOB_VAR = 'gty-player-dob';
|
|
25
|
+
const GTY_TIME_PER_QUESTION_VAR = 'gty-time-per-question';
|
|
26
|
+
const DEFAULT_TIME_PER_QUESTION: number = 10;
|
|
26
27
|
|
|
27
28
|
@customElement("guess-the-year")
|
|
28
29
|
export class GuessTheYear extends LitElement {
|
|
@@ -113,42 +114,48 @@ export class GuessTheYear extends LitElement {
|
|
|
113
114
|
@property({ type: Boolean, attribute: "suppress-images" })
|
|
114
115
|
suppressImages: boolean = false;
|
|
115
116
|
|
|
116
|
-
@property({ type: Number, attribute: "max-hints" })
|
|
117
|
-
maxHints: number = 3;
|
|
118
|
-
|
|
119
117
|
@property({ type: Number, attribute: "number-of-questions", reflect: true })
|
|
120
118
|
numberOfQuestions: number = parseInt(window.localStorage.getItem(GTY_NUM_QUESTIONS_VAR) || MAX_QUESTIONS + '', 10);
|
|
121
119
|
private _numberOfQuestionsSeen: number = 0;
|
|
122
120
|
|
|
123
121
|
private _playerDob: number = parseInt(window.localStorage.getItem(GTY_PLAYER_DOB_VAR) || 0 + '', 10);
|
|
124
122
|
|
|
125
|
-
@
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
private _answers: Array<Number> = [];
|
|
123
|
+
@property({ type: Number, attribute: "time-per-question" })
|
|
124
|
+
timePerQuestion: number = parseInt(window.localStorage.getItem(GTY_TIME_PER_QUESTION_VAR) || DEFAULT_TIME_PER_QUESTION + '', 10);
|
|
129
125
|
|
|
130
126
|
@state()
|
|
131
|
-
private
|
|
127
|
+
private _incident: Incident | undefined = undefined;
|
|
132
128
|
|
|
133
|
-
private _showHint: boolean = true;
|
|
134
129
|
private _showFooter: boolean = false;
|
|
135
|
-
private _hintsIndex: number = 0;
|
|
136
|
-
private _hints: Array<Incident> = [];
|
|
137
130
|
|
|
138
131
|
@state()
|
|
139
|
-
private _solution:
|
|
132
|
+
private _solution: number | undefined = undefined;
|
|
140
133
|
|
|
141
134
|
@state()
|
|
142
135
|
public _renderState: string = GameState.INIT;
|
|
143
136
|
|
|
144
|
-
@state()
|
|
145
|
-
private _score = 30;
|
|
146
|
-
|
|
147
137
|
public totalScore = 0;
|
|
148
138
|
|
|
149
|
-
//
|
|
139
|
+
// Timeline state
|
|
140
|
+
@state()
|
|
141
|
+
private _timelineYears: number[] = [];
|
|
142
|
+
|
|
150
143
|
@state()
|
|
151
|
-
private
|
|
144
|
+
private _clickedPosition: number | undefined = undefined;
|
|
145
|
+
|
|
146
|
+
@state()
|
|
147
|
+
private _correctYear: number | undefined = undefined;
|
|
148
|
+
|
|
149
|
+
private _scoreAddedForCurrentQuestion: boolean = false;
|
|
150
|
+
private _userClicked: boolean = false; // Track if user actually clicked (vs timeout)
|
|
151
|
+
|
|
152
|
+
@state()
|
|
153
|
+
private _toastMessage: string | undefined = undefined;
|
|
154
|
+
|
|
155
|
+
@state()
|
|
156
|
+
private _toastType: 'correct' | 'wrong' | undefined = undefined;
|
|
157
|
+
|
|
158
|
+
private _toastTimeout: number | undefined = undefined;
|
|
152
159
|
|
|
153
160
|
private _apiService = new ApiService(this.teeeApiUrl);
|
|
154
161
|
private i18n = i18nFactory("nl");
|
|
@@ -159,7 +166,23 @@ export class GuessTheYear extends LitElement {
|
|
|
159
166
|
|
|
160
167
|
constructor() {
|
|
161
168
|
super();
|
|
162
|
-
|
|
169
|
+
// Initialize with default, will be reset with actual timePerQuestion when needed
|
|
170
|
+
this._score0 = new QuestionScoreController(this, DEFAULT_TIME_PER_QUESTION, 1000, () => {
|
|
171
|
+
this._handleTimeUp();
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Handles when time runs out - treat as wrong answer
|
|
176
|
+
private _handleTimeUp(): void {
|
|
177
|
+
// If user hasn't clicked yet, treat as wrong answer
|
|
178
|
+
if (!this._userClicked) {
|
|
179
|
+
// Don't set _clickedPosition - timeout is always wrong
|
|
180
|
+
// Process feedback (will be wrong since no answer was given)
|
|
181
|
+
this._processFeedback();
|
|
182
|
+
|
|
183
|
+
// Request update to show feedback
|
|
184
|
+
this.requestUpdate();
|
|
185
|
+
}
|
|
163
186
|
}
|
|
164
187
|
|
|
165
188
|
override connectedCallback() {
|
|
@@ -215,6 +238,7 @@ export class GuessTheYear extends LitElement {
|
|
|
215
238
|
${this._renderState == GameState.PLAY ? this.renderGameStage() : ""}
|
|
216
239
|
${this._renderState == GameState.END ? this.renderGameEnd() : ""}
|
|
217
240
|
${this._renderFooter()}
|
|
241
|
+
${this._renderToast()}
|
|
218
242
|
</div>
|
|
219
243
|
`;
|
|
220
244
|
}
|
|
@@ -224,13 +248,9 @@ export class GuessTheYear extends LitElement {
|
|
|
224
248
|
this?.shadowRoot?.querySelector(element)?.style.setProperty("color", color);
|
|
225
249
|
}
|
|
226
250
|
|
|
227
|
-
// this renders the game header
|
|
228
|
-
private _renderHeader(
|
|
229
|
-
return html
|
|
230
|
-
<section id="top" class="header guess-the-year-header">
|
|
231
|
-
<h1>${content}</h1>
|
|
232
|
-
</section>
|
|
233
|
-
`;
|
|
251
|
+
// this renders the game header (disabled for tile theme)
|
|
252
|
+
private _renderHeader(_content: string = this.i18n.translate("game-name")): TemplateResult {
|
|
253
|
+
return html``;
|
|
234
254
|
}
|
|
235
255
|
|
|
236
256
|
// this renders the game footer
|
|
@@ -300,6 +320,17 @@ export class GuessTheYear extends LitElement {
|
|
|
300
320
|
`);
|
|
301
321
|
}
|
|
302
322
|
|
|
323
|
+
// pre-render time per question options
|
|
324
|
+
const timeOptions = [2, 5, 10, 15];
|
|
325
|
+
const selectTimeOptions = [];
|
|
326
|
+
for (const time of timeOptions) {
|
|
327
|
+
selectTimeOptions.push(html`
|
|
328
|
+
<option value="${time}" ?selected=${this.timePerQuestion == time}>
|
|
329
|
+
${time}
|
|
330
|
+
</option>
|
|
331
|
+
`);
|
|
332
|
+
}
|
|
333
|
+
|
|
303
334
|
return html`
|
|
304
335
|
${this._renderHeader(this.i18n.translate("game-introduction-welcome"))}
|
|
305
336
|
|
|
@@ -307,15 +338,8 @@ export class GuessTheYear extends LitElement {
|
|
|
307
338
|
<div class="game-explanation">
|
|
308
339
|
<p>
|
|
309
340
|
${this.i18n.translate("game-introduction-p1")}
|
|
310
|
-
</p>
|
|
311
|
-
|
|
312
|
-
<p>
|
|
313
341
|
${this.i18n.translate("game-introduction-p2")}
|
|
314
342
|
</p>
|
|
315
|
-
|
|
316
|
-
<p>
|
|
317
|
-
${this.i18n.translate("game-introduction-p3")}
|
|
318
|
-
</p>
|
|
319
343
|
</div>
|
|
320
344
|
|
|
321
345
|
<div class="game-settings">
|
|
@@ -327,6 +351,11 @@ export class GuessTheYear extends LitElement {
|
|
|
327
351
|
${selectQuestionOptions}
|
|
328
352
|
</select>
|
|
329
353
|
${this.i18n.translate("game-settings-p1-2")}
|
|
354
|
+
${this.i18n.translate("game-settings-time-1")}
|
|
355
|
+
<select @change="${this._setTimePerQuestion}">
|
|
356
|
+
${selectTimeOptions}
|
|
357
|
+
</select>
|
|
358
|
+
${this.i18n.translate("game-settings-time-2")}
|
|
330
359
|
${this.i18n.translate("game-settings-p2")}
|
|
331
360
|
<select @change="${this._setFromDate}">
|
|
332
361
|
${this._renderYearStepXSelectOptions(MIN_YEAR, MAX_YEAR, 5, this.from.format("YYYY"))}
|
|
@@ -360,6 +389,11 @@ export class GuessTheYear extends LitElement {
|
|
|
360
389
|
window.localStorage.setItem(GTY_NUM_QUESTIONS_VAR, this.numberOfQuestions + '');
|
|
361
390
|
}
|
|
362
391
|
|
|
392
|
+
private _setTimePerQuestion(event: any): void {
|
|
393
|
+
this.timePerQuestion = Number(event.target.value);
|
|
394
|
+
window.localStorage.setItem(GTY_TIME_PER_QUESTION_VAR, this.timePerQuestion + '');
|
|
395
|
+
}
|
|
396
|
+
|
|
363
397
|
private _setFromDate(event: any): void {
|
|
364
398
|
this.from = this.from.year(event.target.value);
|
|
365
399
|
window.localStorage.setItem(GTY_FROM_YEAR_VAR, this.from.format('YYYY') + '');
|
|
@@ -381,16 +415,12 @@ export class GuessTheYear extends LitElement {
|
|
|
381
415
|
|
|
382
416
|
<div class="container">
|
|
383
417
|
<div class="incident">${this._renderIncident()}</div>
|
|
384
|
-
${this._renderHint()}
|
|
385
418
|
<div class="sources">${this.renderSources()}</div>
|
|
386
419
|
</div>
|
|
387
420
|
|
|
388
421
|
<div class="game-controls">
|
|
389
|
-
<div class="game-controls--
|
|
390
|
-
<div class="game-controls--answers">${this._renderAnswers()}</div>
|
|
422
|
+
<div class="game-controls--timeline">${this._renderTimeline()}</div>
|
|
391
423
|
</div>
|
|
392
|
-
|
|
393
|
-
${this._renderFeedback()}
|
|
394
424
|
</div>
|
|
395
425
|
`;
|
|
396
426
|
}
|
|
@@ -441,23 +471,82 @@ export class GuessTheYear extends LitElement {
|
|
|
441
471
|
return html``;
|
|
442
472
|
}
|
|
443
473
|
|
|
444
|
-
// renders
|
|
445
|
-
private
|
|
474
|
+
// renders the timeline for relative year positioning
|
|
475
|
+
private _renderTimeline(): TemplateResult {
|
|
476
|
+
const fromYear = this.from.year();
|
|
477
|
+
const toYear = this.to.year();
|
|
478
|
+
const timelineRange = toYear - fromYear;
|
|
479
|
+
|
|
480
|
+
// Calculate positions for known years on timeline (0-100%)
|
|
481
|
+
const yearPositions = this._timelineYears.map(year => {
|
|
482
|
+
const position = ((year - fromYear) / timelineRange) * 100;
|
|
483
|
+
return { year, position: Math.max(0, Math.min(100, position)) };
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// Calculate clicked position if available
|
|
487
|
+
let clickedPositionPercent: number | undefined = undefined;
|
|
488
|
+
if (this._clickedPosition !== undefined) {
|
|
489
|
+
clickedPositionPercent = ((this._clickedPosition - fromYear) / timelineRange) * 100;
|
|
490
|
+
clickedPositionPercent = Math.max(0, Math.min(100, clickedPositionPercent));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Calculate correct position if available
|
|
494
|
+
let correctPositionPercent: number | undefined = undefined;
|
|
495
|
+
if (this._correctYear !== undefined) {
|
|
496
|
+
correctPositionPercent = ((this._correctYear - fromYear) / timelineRange) * 100;
|
|
497
|
+
correctPositionPercent = Math.max(0, Math.min(100, correctPositionPercent));
|
|
498
|
+
}
|
|
499
|
+
|
|
446
500
|
return html`
|
|
447
|
-
<div class="
|
|
448
|
-
<
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
501
|
+
<div class="timeline-container">
|
|
502
|
+
<div
|
|
503
|
+
class="timeline"
|
|
504
|
+
@click="${this._handleTimelineClick}"
|
|
505
|
+
>
|
|
506
|
+
<!-- Timeline line -->
|
|
507
|
+
<div class="timeline-line"></div>
|
|
508
|
+
|
|
509
|
+
<!-- Year markers (tegeltjes) -->
|
|
510
|
+
${yearPositions.map(({ year, position }) => html`
|
|
511
|
+
<div
|
|
512
|
+
class="timeline-tile"
|
|
513
|
+
style="left: ${position}%;"
|
|
514
|
+
>
|
|
515
|
+
<div class="timeline-tile-content">
|
|
516
|
+
${year}
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
`)}
|
|
520
|
+
|
|
521
|
+
<!-- Clicked position indicator -->
|
|
522
|
+
${clickedPositionPercent !== undefined ? html`
|
|
523
|
+
<div
|
|
524
|
+
class="timeline-clicked ${this._isTimelinePositionCorrect() ? 'timeline-clicked-correct' : 'timeline-clicked-wrong'}"
|
|
525
|
+
style="left: ${clickedPositionPercent}%;"
|
|
526
|
+
></div>
|
|
527
|
+
` : ''}
|
|
528
|
+
|
|
529
|
+
<!-- Correct position indicator (shown after answer) -->
|
|
530
|
+
${correctPositionPercent !== undefined && this._clickedPosition !== undefined ? html`
|
|
531
|
+
<div
|
|
532
|
+
class="timeline-correct"
|
|
533
|
+
style="left: ${correctPositionPercent}%;"
|
|
534
|
+
></div>
|
|
535
|
+
` : ''}
|
|
536
|
+
|
|
537
|
+
<!-- Year labels at start and end -->
|
|
538
|
+
<div class="timeline-year-start">${fromYear}</div>
|
|
539
|
+
<div class="timeline-year-end">${toYear}</div>
|
|
540
|
+
</div>
|
|
454
541
|
</div>
|
|
455
542
|
`;
|
|
456
543
|
}
|
|
457
544
|
|
|
458
545
|
// renders the current question score
|
|
459
|
-
// it counts back to 0
|
|
460
546
|
private _renderGameScore(): TemplateResult {
|
|
547
|
+
// Show the countdown score (remaining time/points)
|
|
548
|
+
const currentQuestionScore = Math.max(0, this._score0.value);
|
|
549
|
+
|
|
461
550
|
return html`
|
|
462
551
|
<div class="game-score--total">
|
|
463
552
|
<div class="game-score--label">${this.i18n.translate("question-number")}:</div>
|
|
@@ -468,7 +557,7 @@ export class GuessTheYear extends LitElement {
|
|
|
468
557
|
|
|
469
558
|
<div class="game-score--score">
|
|
470
559
|
<div class="game-score--label">${this.i18n.translate("question-score")}:</div>
|
|
471
|
-
<div class="game-score--value">${
|
|
560
|
+
<div class="game-score--value">${currentQuestionScore}</div>
|
|
472
561
|
</div>
|
|
473
562
|
|
|
474
563
|
<div class="game-score--total">
|
|
@@ -478,157 +567,208 @@ export class GuessTheYear extends LitElement {
|
|
|
478
567
|
`;
|
|
479
568
|
}
|
|
480
569
|
|
|
481
|
-
// handles the click on
|
|
482
|
-
private
|
|
483
|
-
|
|
484
|
-
|
|
570
|
+
// handles the click on the timeline
|
|
571
|
+
private _handleTimelineClick(event: MouseEvent): void {
|
|
572
|
+
if (!this._correctYear) {
|
|
573
|
+
return; // No question loaded yet
|
|
574
|
+
}
|
|
485
575
|
|
|
486
|
-
|
|
576
|
+
const timelineElement = event.currentTarget as HTMLElement;
|
|
577
|
+
const rect = timelineElement.getBoundingClientRect();
|
|
578
|
+
const clickX = event.clientX - rect.left;
|
|
579
|
+
const clickPercentage = (clickX / rect.width) * 100;
|
|
580
|
+
|
|
581
|
+
const fromYear = this.from.year();
|
|
582
|
+
const toYear = this.to.year();
|
|
583
|
+
const timelineRange = toYear - fromYear;
|
|
584
|
+
|
|
585
|
+
// Convert click position to year
|
|
586
|
+
const clickedYear = Math.round(fromYear + (clickPercentage / 100) * timelineRange);
|
|
587
|
+
|
|
588
|
+
// Clamp to valid range
|
|
589
|
+
const clampedYear = Math.max(fromYear, Math.min(toYear, clickedYear));
|
|
590
|
+
|
|
591
|
+
this._clickedPosition = clampedYear;
|
|
592
|
+
this._userClicked = true; // Mark that user actually clicked
|
|
593
|
+
|
|
594
|
+
// Stop the score counter (if still running)
|
|
487
595
|
this._score0.stop();
|
|
488
596
|
|
|
489
|
-
//
|
|
490
|
-
this.
|
|
491
|
-
|
|
492
|
-
//
|
|
493
|
-
|
|
494
|
-
// this._apiService.postIncidentAnswer(this._incident?.id, answer);
|
|
495
|
-
console.debug(
|
|
496
|
-
answer,
|
|
497
|
-
this._solution,
|
|
498
|
-
this._score,
|
|
499
|
-
answer == this._solution,
|
|
500
|
-
);
|
|
501
|
-
|
|
502
|
-
this._answer = answer;
|
|
503
|
-
}
|
|
597
|
+
// Process feedback and show toast
|
|
598
|
+
this._processFeedback();
|
|
599
|
+
|
|
600
|
+
// Request update to show feedback
|
|
601
|
+
this.requestUpdate();
|
|
504
602
|
}
|
|
505
603
|
|
|
506
|
-
//
|
|
507
|
-
private
|
|
508
|
-
if (
|
|
509
|
-
return
|
|
604
|
+
// checks if the clicked position is correct (between older and younger events)
|
|
605
|
+
private _isTimelinePositionCorrect(): boolean {
|
|
606
|
+
if (this._clickedPosition === undefined || this._correctYear === undefined) {
|
|
607
|
+
return false;
|
|
510
608
|
}
|
|
511
609
|
|
|
512
|
-
|
|
610
|
+
// Find the position where the correct year should be inserted
|
|
611
|
+
const sortedKnownYears = [...this._timelineYears].sort((a, b) => a - b);
|
|
612
|
+
|
|
613
|
+
// Find the two years that bracket the correct year
|
|
614
|
+
let lowerBound: number | null = null;
|
|
615
|
+
let upperBound: number | null = null;
|
|
616
|
+
|
|
617
|
+
for (let i = 0; i < sortedKnownYears.length; i++) {
|
|
618
|
+
if (sortedKnownYears[i] < this._correctYear) {
|
|
619
|
+
lowerBound = sortedKnownYears[i];
|
|
620
|
+
} else if (sortedKnownYears[i] > this._correctYear && upperBound === null) {
|
|
621
|
+
upperBound = sortedKnownYears[i];
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// If no lower bound, use the start of the timeline
|
|
627
|
+
if (lowerBound === null) {
|
|
628
|
+
lowerBound = this.from.year();
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// If no upper bound, use the end of the timeline
|
|
632
|
+
if (upperBound === null) {
|
|
633
|
+
upperBound = this.to.year();
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Check if clicked position is between lower and upper bound
|
|
637
|
+
return this._clickedPosition >= lowerBound && this._clickedPosition <= upperBound;
|
|
513
638
|
}
|
|
514
639
|
|
|
515
|
-
// renders the hint button until
|
|
516
|
-
// all max hints have been used
|
|
517
|
-
private _renderHintButton(): TemplateResult {
|
|
518
|
-
if (!this._showHint) {
|
|
519
|
-
return html``;
|
|
520
|
-
}
|
|
521
640
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
641
|
+
/**
|
|
642
|
+
* Processes feedback and shows toast notification
|
|
643
|
+
*/
|
|
644
|
+
private _processFeedback(): void {
|
|
645
|
+
// Stop score counter if still running
|
|
646
|
+
if (this._score0.isRunning()) {
|
|
647
|
+
this._score0.stop();
|
|
648
|
+
}
|
|
530
649
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
private _handleHintClicked(): void {
|
|
536
|
-
console.debug("handleHintClicked", [this._hintsIndex, this._hints.length]);
|
|
650
|
+
// If user didn't click (timeout), always treat as wrong
|
|
651
|
+
if (!this._userClicked) {
|
|
652
|
+
const is_correct = false;
|
|
653
|
+
const score = 0;
|
|
537
654
|
|
|
538
|
-
|
|
539
|
-
|
|
655
|
+
// Add score to total (only once per question)
|
|
656
|
+
if (!this._scoreAddedForCurrentQuestion) {
|
|
657
|
+
this.totalScore += score;
|
|
658
|
+
this._scoreAddedForCurrentQuestion = true;
|
|
659
|
+
}
|
|
540
660
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
this.
|
|
544
|
-
this._score0.subtract(HINT_COST);
|
|
661
|
+
// Fire-and-forget recallability feedback.
|
|
662
|
+
if (this._incident?.id) {
|
|
663
|
+
this._apiService.incidentRecallability(this._incident?.id, this._playerDob, is_correct);
|
|
545
664
|
}
|
|
546
|
-
}
|
|
547
665
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
666
|
+
// Show toast notification
|
|
667
|
+
const message = this.i18n.translate("timeline-wrong")
|
|
668
|
+
.replace(/__YEAR__/, (this._correctYear || 0).toString())
|
|
669
|
+
.replace(/__YEAR1__/, this.from.year().toString())
|
|
670
|
+
.replace(/__YEAR2__/, this.to.year().toString());
|
|
671
|
+
this._showToast(message, 'wrong');
|
|
672
|
+
return;
|
|
551
673
|
}
|
|
552
|
-
}
|
|
553
674
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
// and the score is greater than 0
|
|
558
|
-
if (!this._answer && this._score0.value > 0) {
|
|
559
|
-
return html``;
|
|
675
|
+
// do not process if no answer has been made
|
|
676
|
+
if (!this._clickedPosition) {
|
|
677
|
+
return;
|
|
560
678
|
}
|
|
561
679
|
|
|
562
|
-
// check if the
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
const score = is_correct ? this._score0.value : 0;
|
|
680
|
+
// check if the timeline position is correct
|
|
681
|
+
const is_correct = this._isTimelinePositionCorrect();
|
|
682
|
+
// Use remaining time as score (or 0 if wrong)
|
|
683
|
+
const score = is_correct ? Math.max(0, this._score0.value) : 0;
|
|
566
684
|
|
|
567
|
-
|
|
568
|
-
|
|
685
|
+
// Add score to total (only once per question)
|
|
686
|
+
if (!this._scoreAddedForCurrentQuestion) {
|
|
687
|
+
this.totalScore += score;
|
|
688
|
+
this._scoreAddedForCurrentQuestion = true;
|
|
569
689
|
}
|
|
570
690
|
|
|
571
|
-
|
|
691
|
+
// Add correct year to timeline only if answer is correct
|
|
692
|
+
if (is_correct && this._correctYear !== undefined) {
|
|
693
|
+
this._addYearToTimeline(this._correctYear);
|
|
694
|
+
}
|
|
572
695
|
|
|
573
696
|
// Fire-and-forget recallability feedback.
|
|
574
697
|
if (this._incident?.id) {
|
|
575
698
|
this._apiService.incidentRecallability(this._incident?.id, this._playerDob, is_correct);
|
|
576
699
|
}
|
|
577
700
|
|
|
701
|
+
// Find the bounds for the correct position
|
|
702
|
+
const sortedKnownYears = [...this._timelineYears].sort((a, b) => a - b);
|
|
703
|
+
let lowerBound: number = this.from.year();
|
|
704
|
+
let upperBound: number = this.to.year();
|
|
705
|
+
|
|
706
|
+
if (this._correctYear !== undefined) {
|
|
707
|
+
for (let i = 0; i < sortedKnownYears.length; i++) {
|
|
708
|
+
if (sortedKnownYears[i] < this._correctYear) {
|
|
709
|
+
lowerBound = sortedKnownYears[i];
|
|
710
|
+
} else if (sortedKnownYears[i] > this._correctYear && upperBound === this.to.year()) {
|
|
711
|
+
upperBound = sortedKnownYears[i];
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Show toast message
|
|
578
718
|
if (is_correct) {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
<p>
|
|
584
|
-
${this._hintsIndex == 1 ? this.i18n.translate("used-hint-singular") : this.i18n.translate("used-hint-plural").replace(/__HINTS__/, this._hintsIndex + '')}
|
|
585
|
-
</p>
|
|
586
|
-
<p>
|
|
587
|
-
${score == 1 ? this.i18n.translate("number-points-singular") : this.i18n.translate("number-points-plural").replace(/__POINTS__/, score + '')}
|
|
588
|
-
</p>
|
|
589
|
-
|
|
590
|
-
${this._renderNextQuestionButton()}
|
|
591
|
-
</div>
|
|
592
|
-
</div>
|
|
593
|
-
`;
|
|
594
|
-
} else if (!is_correct && this._answer) {
|
|
595
|
-
return html`
|
|
596
|
-
<div class="feedback">
|
|
597
|
-
<div class="feedback--content">
|
|
598
|
-
<h3>${this.i18n.translate("answer-wrong")}</h3>
|
|
599
|
-
<p>
|
|
600
|
-
${this.i18n.translate("correct-answer-was").replace(/__WRONG__/, this._answer + '').replace(/__CORRECT__/, this._solution + '')}
|
|
601
|
-
</p>
|
|
602
|
-
${this._renderNextQuestionButton()}
|
|
603
|
-
</div>
|
|
604
|
-
</div>
|
|
605
|
-
`;
|
|
719
|
+
this._showToast(
|
|
720
|
+
this.i18n.translate("timeline-correct-with-year").replace(/__YEAR__/, this._correctYear + '').replace(/__YEAR1__/, lowerBound + '').replace(/__YEAR2__/, upperBound + ''),
|
|
721
|
+
'correct'
|
|
722
|
+
);
|
|
606
723
|
} else {
|
|
724
|
+
this._showToast(
|
|
725
|
+
this.i18n.translate("timeline-wrong").replace(/__YEAR__/, this._correctYear + '').replace(/__YEAR1__/, lowerBound + '').replace(/__YEAR2__/, upperBound + ''),
|
|
726
|
+
'wrong'
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Renders the toast notification
|
|
732
|
+
private _renderToast(): TemplateResult {
|
|
733
|
+
if (this._toastMessage && this._toastType) {
|
|
607
734
|
return html`
|
|
608
|
-
<div class="
|
|
609
|
-
<div class="
|
|
610
|
-
<
|
|
611
|
-
<p>
|
|
612
|
-
${this.i18n.translate("no-answer-correct-answer-was").replace(/__CORRECT__/, this._solution + '')}
|
|
613
|
-
</p>
|
|
614
|
-
${this._renderNextQuestionButton()}
|
|
735
|
+
<div class="toast toast-${this._toastType}">
|
|
736
|
+
<div class="toast-content">
|
|
737
|
+
<p>${this._toastMessage}</p>
|
|
615
738
|
</div>
|
|
616
739
|
</div>
|
|
617
740
|
`;
|
|
618
741
|
}
|
|
742
|
+
return html``;
|
|
619
743
|
}
|
|
620
744
|
|
|
621
|
-
//
|
|
622
|
-
private
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
`;
|
|
745
|
+
// Shows a toast notification that auto-dismisses after 3 seconds
|
|
746
|
+
private _showToast(message: string, type: 'correct' | 'wrong'): void {
|
|
747
|
+
// Clear any existing timeout
|
|
748
|
+
if (this._toastTimeout) {
|
|
749
|
+
clearTimeout(this._toastTimeout);
|
|
627
750
|
}
|
|
628
751
|
|
|
629
|
-
|
|
752
|
+
this._toastMessage = message;
|
|
753
|
+
this._toastType = type;
|
|
754
|
+
this.requestUpdate();
|
|
755
|
+
|
|
756
|
+
// Auto-dismiss after 3 seconds and load next question
|
|
757
|
+
this._toastTimeout = setTimeout(() => {
|
|
758
|
+
this._toastMessage = undefined;
|
|
759
|
+
this._toastType = undefined;
|
|
760
|
+
this.requestUpdate();
|
|
761
|
+
|
|
762
|
+
// Automatically load next question or end game
|
|
763
|
+
if (this._numberOfQuestionsSeen < this.numberOfQuestions) {
|
|
764
|
+
this._loadQuestion();
|
|
765
|
+
} else {
|
|
766
|
+
this._endGame();
|
|
767
|
+
}
|
|
768
|
+
}, 3000);
|
|
630
769
|
}
|
|
631
770
|
|
|
771
|
+
|
|
632
772
|
// renders the end of the game
|
|
633
773
|
private _endGame(): void {
|
|
634
774
|
this._renderState = GameState.END;
|
|
@@ -636,10 +776,10 @@ export class GuessTheYear extends LitElement {
|
|
|
636
776
|
|
|
637
777
|
private renderGameEnd(): TemplateResult {
|
|
638
778
|
console.debug("renderGameEnd");
|
|
639
|
-
const maxScore: number = this.numberOfQuestions * this.
|
|
779
|
+
const maxScore: number = this.numberOfQuestions * this.timePerQuestion; // max points per question
|
|
640
780
|
const scoreRatio = Math.round((this.totalScore / maxScore) * 100);
|
|
641
781
|
return html`
|
|
642
|
-
${this._renderHeader("
|
|
782
|
+
${this._renderHeader(this.i18n.translate("the-end"))}
|
|
643
783
|
<div class="guess-the-year-game-end">
|
|
644
784
|
<p>
|
|
645
785
|
${this.i18n.translate("end").replace(/__SCORE__/, this.totalScore + '').replace(/__MAX_SCORE__/, maxScore + '').replace(/__PERCENT__/, scoreRatio + '')}
|
|
@@ -667,19 +807,41 @@ export class GuessTheYear extends LitElement {
|
|
|
667
807
|
this.totalScore = 0;
|
|
668
808
|
this._numberOfQuestionsSeen = 0;
|
|
669
809
|
|
|
810
|
+
// Initialize timeline with middle of the period
|
|
811
|
+
const middleYear = Math.floor((this.from.year() + this.to.year()) / 2);
|
|
812
|
+
this._timelineYears = [middleYear];
|
|
813
|
+
|
|
670
814
|
this._loadQuestion();
|
|
671
815
|
}
|
|
672
816
|
|
|
673
817
|
private _resetStage(): void {
|
|
674
818
|
this.sources = [];
|
|
675
819
|
this._incident = undefined;
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
this.
|
|
679
|
-
|
|
820
|
+
|
|
821
|
+
// Reset user interaction flags
|
|
822
|
+
this._userClicked = false;
|
|
823
|
+
|
|
824
|
+
// Update score controller with current timePerQuestion value
|
|
825
|
+
this._score0.startValue = this.timePerQuestion;
|
|
680
826
|
this._score0.reset();
|
|
681
|
-
|
|
682
|
-
this.
|
|
827
|
+
// Reset timeline interaction state, but keep known years
|
|
828
|
+
this._clickedPosition = undefined;
|
|
829
|
+
this._correctYear = undefined;
|
|
830
|
+
this._scoreAddedForCurrentQuestion = false;
|
|
831
|
+
|
|
832
|
+
// Clear toast
|
|
833
|
+
if (this._toastTimeout) {
|
|
834
|
+
clearTimeout(this._toastTimeout);
|
|
835
|
+
this._toastTimeout = undefined;
|
|
836
|
+
}
|
|
837
|
+
this._toastMessage = undefined;
|
|
838
|
+
this._toastType = undefined;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
private _addYearToTimeline(year: number): void {
|
|
842
|
+
if (!this._timelineYears.includes(year)) {
|
|
843
|
+
this._timelineYears = [...this._timelineYears, year].sort((a, b) => a - b);
|
|
844
|
+
}
|
|
683
845
|
}
|
|
684
846
|
|
|
685
847
|
async _loadQuestion(): Promise<void> {
|
|
@@ -701,54 +863,13 @@ export class GuessTheYear extends LitElement {
|
|
|
701
863
|
this._incident = response.incident;
|
|
702
864
|
this.sources = response.sources;
|
|
703
865
|
|
|
704
|
-
this._solution =
|
|
866
|
+
this._solution = parseInt(
|
|
705
867
|
(response.incident?.yearplusmonth || "").substring(0, 4),
|
|
706
|
-
|
|
868
|
+
10
|
|
869
|
+
) || undefined;
|
|
707
870
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
const to = this.to.year();
|
|
711
|
-
|
|
712
|
-
// generate 3 more random and unique answers between to and from
|
|
713
|
-
while (answers.length < 4) {
|
|
714
|
-
const randomAnswer = Math.floor(Math.random() * (to - from)) + from;
|
|
715
|
-
if (!answers.includes(randomAnswer)) {
|
|
716
|
-
answers.push(randomAnswer);
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
// sort theses answers
|
|
721
|
-
answers.sort();
|
|
722
|
-
|
|
723
|
-
this._answers = answers;
|
|
724
|
-
|
|
725
|
-
for (let i = 0; i < this.maxHints; i++) {
|
|
726
|
-
console.debug("loading hint", i);
|
|
727
|
-
|
|
728
|
-
const response: { incident: Incident | undefined; sources: string[] } =
|
|
729
|
-
await this._apiService.fetchOneIncident(
|
|
730
|
-
this.country,
|
|
731
|
-
Util.shuffleArray([...Categories]).shift(),
|
|
732
|
-
undefined,
|
|
733
|
-
undefined,
|
|
734
|
-
"",
|
|
735
|
-
`${this._solution}-01-01`,
|
|
736
|
-
`${this._solution}-12-31`,
|
|
737
|
-
undefined,
|
|
738
|
-
undefined,
|
|
739
|
-
);
|
|
740
|
-
|
|
741
|
-
if (response.incident) {
|
|
742
|
-
this._hints.push(response.incident);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
if (this._hints.length === 0) {
|
|
747
|
-
this._showHint = false;
|
|
748
|
-
}
|
|
749
|
-
else {
|
|
750
|
-
this._showHint = true;
|
|
751
|
-
}
|
|
871
|
+
// Store correct year for timeline
|
|
872
|
+
this._correctYear = this._solution;
|
|
752
873
|
|
|
753
874
|
// start render game stage
|
|
754
875
|
this._renderState = GameState.PLAY;
|
|
@@ -760,65 +881,6 @@ export class GuessTheYear extends LitElement {
|
|
|
760
881
|
this._numberOfQuestionsSeen += 1;
|
|
761
882
|
}
|
|
762
883
|
|
|
763
|
-
private _generateHintText(hintIncident: Incident): string {
|
|
764
|
-
let hint = "";
|
|
765
|
-
const hintTemplates = this.i18n.HintTemplates[hintIncident.category];
|
|
766
|
-
switch (hintIncident.category) {
|
|
767
|
-
case Category.newsItem:
|
|
768
|
-
hint = (
|
|
769
|
-
hintTemplates[Math.round(Math.random() * hintTemplates.length)] || ""
|
|
770
|
-
).replace(/__TITLE__/, hintIncident.title);
|
|
771
|
-
break;
|
|
772
|
-
case Category.radioSong:
|
|
773
|
-
// The title property of radioSongs always follow this pattern: track - artist
|
|
774
|
-
const track = hintIncident.title.replace(/(.*?)\s+\-.*/, "$1");
|
|
775
|
-
const artist = hintIncident.title.replace(/.*\-(.*)/, "$1");
|
|
776
|
-
hint = (
|
|
777
|
-
hintTemplates[Math.round(Math.random() * hintTemplates.length)] ||
|
|
778
|
-
hintTemplates[0]
|
|
779
|
-
)
|
|
780
|
-
.replace(/__ARTIST__/, artist)
|
|
781
|
-
.replace(/__TRACK__/, track);
|
|
782
|
-
break;
|
|
783
|
-
case Category.cinemaMovie:
|
|
784
|
-
hint = (
|
|
785
|
-
hintTemplates[Math.round(Math.random() * hintTemplates.length)] ||
|
|
786
|
-
hintTemplates[0]
|
|
787
|
-
).replace(/__TITLE__/, hintIncident.title);
|
|
788
|
-
break;
|
|
789
|
-
case Category.tech:
|
|
790
|
-
hint = (
|
|
791
|
-
hintTemplates[Math.round(Math.random() * hintTemplates.length)] ||
|
|
792
|
-
hintTemplates[0]
|
|
793
|
-
).replace(/__TITLE__/, hintIncident.title);
|
|
794
|
-
break;
|
|
795
|
-
case Category.newsPresenter:
|
|
796
|
-
hint = (
|
|
797
|
-
hintTemplates[Math.round(Math.random() * hintTemplates.length)] ||
|
|
798
|
-
hintTemplates[0]
|
|
799
|
-
).replace(/__TITLE__/, hintIncident.title || hintIncident.text);
|
|
800
|
-
break;
|
|
801
|
-
case Category.tvShowOrSeries:
|
|
802
|
-
hint = (
|
|
803
|
-
hintTemplates[Math.round(Math.random() * hintTemplates.length)] ||
|
|
804
|
-
hintTemplates[0]
|
|
805
|
-
).replace(/__TITLE__/, hintIncident.title || hintIncident.text);
|
|
806
|
-
break;
|
|
807
|
-
case Category.sports:
|
|
808
|
-
hint = (
|
|
809
|
-
hintTemplates[Math.round(Math.random() * hintTemplates.length)] ||
|
|
810
|
-
hintTemplates[0]
|
|
811
|
-
).replace(/__TITLE__/, hintIncident.title || hintIncident.text);
|
|
812
|
-
break;
|
|
813
|
-
case Category.showbizz:
|
|
814
|
-
hint = (
|
|
815
|
-
hintTemplates[Math.round(Math.random() * hintTemplates.length)] ||
|
|
816
|
-
hintTemplates[0]
|
|
817
|
-
).replace(/__TITLE__/, hintIncident.title || hintIncident.text);
|
|
818
|
-
break;
|
|
819
|
-
}
|
|
820
|
-
return hint;
|
|
821
|
-
}
|
|
822
884
|
|
|
823
885
|
renderSources(): TemplateResult {
|
|
824
886
|
console.debug("renderSources");
|