Skip to content

Commit dfe3114

Browse files
CYX22222003reginatehCZX123
authored
Game: Create a new quiz feature (#2919)
* Add new action show_quiz * Add quiz parser * Add quiz manager * Create quiz result type * Hide dialogue box while showing quiz * Modify character hidding feature * Track and display quiz results * Debugging for speaker displaying * Modify UI for quiz displaying * Update UI for quiz display * Solve quiz speaker issue * Remove default reaction in QuizParser and handle no reaction situation in QuizManager * Add a new property (boolean array) in Quiz type to record quiz result * Rearrange quiz result * Save quiz status as attempted/completed arrays * Add conditions to check whether a quiz is attempted/completed * Disable keyboard input in a quiz * Move question prompt text to QuizConstants * QuizParser error handling & add new "speaker" property to QuizType & store quiz questions to dialogue log * Display quiz result message to dialogue log * Move quiz result message to QuizConstant * Change quiz condition names * Add a prompt before a quiz & proceed to the next dialogue line when a quiz ends * Add saving quizzes score * Add validation for quiz conditions parameters * Add new condition quizScoreAtLeast to check the status of quiz scores * Support Interpolation of player's name in quiz questions * Support interpolation of quiz scores in dialogue lines * Refactor the logic of makeLineQuizScores method Co-authored-by: reginateh <[email protected]> Co-authored-by: CZX <[email protected]>
1 parent 5b54dc1 commit dfe3114

27 files changed

+910
-11
lines changed

src/features/game/action/GameActionConditionChecker.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ export default class ActionConditionChecker {
4242
return GameGlobalAPI.getInstance().isObjectiveComplete(conditionParams.id) === boolean;
4343
case GameStateStorage.TasklistState:
4444
return GameGlobalAPI.getInstance().isTaskComplete(conditionParams.id) === boolean;
45+
case GameStateStorage.AttemptedQuizState:
46+
return GameGlobalAPI.getInstance().isQuizAttempted(conditionParams.id) === boolean;
47+
case GameStateStorage.PassedQuizState:
48+
return GameGlobalAPI.getInstance().isQuizComplete(conditionParams.id) === boolean;
49+
case GameStateStorage.QuizScoreState:
50+
return (
51+
GameGlobalAPI.getInstance().getQuizScore(conditionParams.id) >=
52+
parseInt(conditionParams.score) ===
53+
boolean
54+
);
4555
default:
4656
return true;
4757
}

src/features/game/action/GameActionExecuter.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ export default class GameActionExecuter {
102102
case GameActionType.Delay:
103103
await sleep(actionParams.duration);
104104
return;
105+
case GameActionType.ShowQuiz:
106+
globalAPI.enableKeyboardInput(false);
107+
await globalAPI.showQuiz(actionParams.id);
108+
globalAPI.enableKeyboardInput(true);
109+
return;
105110
default:
106111
return;
107112
}
@@ -141,6 +146,7 @@ export default class GameActionExecuter {
141146
case GameActionType.PlaySFX:
142147
case GameActionType.ShowObjectLayer:
143148
case GameActionType.Delay:
149+
case GameActionType.ShowQuiz:
144150
return false;
145151
}
146152
}

src/features/game/action/GameActionTypes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export enum GameActionType {
2525
ShowObjectLayer = 'ShowObjectLayer',
2626
NavigateToAssessment = 'NavigateToAssessment',
2727
UpdateAssessmentStatus = 'UpdateAssessmentStatus',
28-
Delay = 'Delay'
28+
Delay = 'Delay',
29+
ShowQuiz = 'ShowQuiz'
2930
}
3031

3132
/**

src/features/game/dialogue/GameDialogueManager.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,12 @@ export default class DialogueManager {
6666
});
6767
}
6868

69-
private async showNextLine(resolve: () => void) {
69+
public async showNextLine(resolve: () => void) {
7070
GameGlobalAPI.getInstance().playSound(SoundAssets.dialogueAdvance.key);
7171
const { line, speakerDetail, actionIds, prompt } =
7272
await this.getDialogueGenerator().generateNextLine();
73-
const lineWithName = line.replace('{name}', this.getUsername());
73+
const lineWithQuizScores = this.makeLineWithQuizScores(line);
74+
const lineWithName = lineWithQuizScores.replace('{name}', this.getUsername());
7475
this.getDialogueRenderer().changeText(lineWithName);
7576
this.getSpeakerRenderer().changeSpeakerTo(speakerDetail);
7677

@@ -79,6 +80,7 @@ export default class DialogueManager {
7980

8081
// Disable interactions while processing actions
8182
GameGlobalAPI.getInstance().enableSprite(this.getDialogueRenderer().getDialogueBox(), false);
83+
this.getInputManager().enableKeyboardInput(false);
8284

8385
if (prompt) {
8486
// disable keyboard input to prevent continue dialogue
@@ -94,6 +96,7 @@ export default class DialogueManager {
9496
}
9597
await GameGlobalAPI.getInstance().processGameActionsInSamePhase(actionIds);
9698
GameGlobalAPI.getInstance().enableSprite(this.getDialogueRenderer().getDialogueBox(), true);
99+
this.getInputManager().enableKeyboardInput(true);
97100

98101
if (!line) {
99102
// clear keyboard listeners when dialogue ends
@@ -102,6 +105,38 @@ export default class DialogueManager {
102105
}
103106
}
104107

108+
/**
109+
* Hide all dialogue boxes, speaker boxes and speaker sprites
110+
* */
111+
public async hideAll() {
112+
await this.getDialogueRenderer().hide();
113+
await this.getSpeakerRenderer().hide();
114+
}
115+
116+
/**
117+
* Make all dialogue boxes, speaker boxes and speaker sprites visible
118+
* */
119+
public async showAll() {
120+
await this.getDialogueRenderer().show();
121+
await this.getSpeakerRenderer().show();
122+
}
123+
124+
/**
125+
* Find patterns of quiz score interpolation in a dialogue line,
126+
* and replace them by actual scores.
127+
* The pattern: "{<quizId>.score}"
128+
*
129+
* @param line
130+
* @returns {string} the given line with all quiz score interpolation replaced by actual scores.
131+
*/
132+
public makeLineWithQuizScores(line: string) {
133+
const quizScores = line.matchAll(/\{(.+?)\.score\}/g);
134+
for (const match of quizScores) {
135+
line = line.replace(match[0], GameGlobalAPI.getInstance().getQuizScore(match[1]).toString());
136+
}
137+
return line;
138+
}
139+
105140
private getDialogueGenerator = () => this.dialogueGenerator as DialogueGenerator;
106141
private getDialogueRenderer = () => this.dialogueRenderer as DialogueRenderer;
107142
private getSpeakerRenderer = () => this.speakerRenderer as DialogueSpeakerRenderer;

src/features/game/dialogue/GameDialogueRenderer.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,24 @@ class DialogueRenderer {
6767
fadeAndDestroy(gameManager, this.getDialogueContainer());
6868
}
6969

70+
/**
71+
* Hide the dialoguebox
72+
*/
73+
public async hide() {
74+
this.typewriter.container.setVisible(false);
75+
this.dialogueBox.setVisible(false);
76+
this.blinkingDiamond.container.setVisible(false);
77+
}
78+
79+
/**
80+
* Make the dialoguebox visible
81+
*/
82+
public async show() {
83+
this.typewriter.container.setVisible(true);
84+
this.dialogueBox.setVisible(true);
85+
this.blinkingDiamond.container.setVisible(true);
86+
}
87+
7088
/**
7189
* Change the text written in the box
7290
*/

src/features/game/dialogue/GameDialogueSpeakerRenderer.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import DialogueConstants, { speakerTextStyle } from './GameDialogueConstants';
1616
*/
1717
export default class DialogueSpeakerRenderer {
1818
private currentSpeakerId?: string;
19+
private speakerSprite?: Phaser.GameObjects.Image;
20+
private speakerSpriteBox?: Phaser.GameObjects.Container;
1921

2022
/**
2123
* Changes the speaker shown in the speaker box and the speaker rendered on screen
@@ -63,6 +65,7 @@ export default class DialogueSpeakerRenderer {
6365
expression,
6466
speakerPosition
6567
);
68+
this.speakerSprite = speakerSprite;
6669
GameGlobalAPI.getInstance().addToLayer(Layer.Speaker, speakerSprite);
6770
}
6871

@@ -90,8 +93,27 @@ export default class DialogueSpeakerRenderer {
9093

9194
container.add([rectangle, speakerText]);
9295
speakerText.text = StringUtils.capitalize(text);
96+
this.speakerSpriteBox = container;
9397
return container;
9498
}
9599

100+
/**
101+
* Hide the speaker box and sprite
102+
*/
103+
public async hide() {
104+
this.getSpeakerSprite().setVisible(false);
105+
this.getSpeakerSpriteBox().setVisible(false);
106+
}
107+
108+
/**
109+
* Show the hidden speaker box and sprite
110+
*/
111+
public async show() {
112+
this.getSpeakerSprite().setVisible(true);
113+
this.getSpeakerSpriteBox().setVisible(true);
114+
}
115+
96116
public getUsername = () => SourceAcademyGame.getInstance().getAccountInfo().name;
117+
private getSpeakerSprite = () => this.speakerSprite as Phaser.GameObjects.Image;
118+
private getSpeakerSpriteBox = () => this.speakerSpriteBox as Phaser.GameObjects.Container;
97119
}

src/features/game/layer/GameLayerTypes.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ export enum Layer {
1212
Escape,
1313
Selector,
1414
Dashboard,
15-
WorkerMessage
15+
WorkerMessage,
16+
QuizSpeakerBox,
17+
QuizSpeaker
1618
}
1719

1820
// Back to Front
@@ -23,9 +25,11 @@ export const defaultLayerSequence = [
2325
Layer.BBox,
2426
Layer.Character,
2527
Layer.Speaker,
28+
Layer.QuizSpeaker,
2629
Layer.PopUp,
2730
Layer.Dialogue,
2831
Layer.SpeakerBox,
32+
Layer.QuizSpeakerBox,
2933
Layer.Effects,
3034
Layer.Dashboard,
3135
Layer.Escape,

src/features/game/location/GameMap.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AssetKey, ItemId } from '../commons/CommonTypes';
66
import { Dialogue } from '../dialogue/GameDialogueTypes';
77
import { GameMode } from '../mode/GameModeTypes';
88
import { ObjectProperty } from '../objects/GameObjectTypes';
9+
import { Quiz } from '../quiz/GameQuizType';
910
import { mandatory } from '../utils/GameUtils';
1011
import { AnyId, GameItemType, GameLocation, LocationId } from './GameMapTypes';
1112

@@ -36,6 +37,7 @@ class GameMap {
3637
private actions: Map<ItemId, GameAction>;
3738
private gameStartActions: ItemId[];
3839
private checkpointCompleteActions: ItemId[];
40+
private quizzes: Map<ItemId, Quiz>;
3941

4042
constructor() {
4143
this.soundAssets = [];
@@ -47,6 +49,7 @@ class GameMap {
4749
this.boundingBoxes = new Map<ItemId, BBoxProperty>();
4850
this.characters = new Map<ItemId, Character>();
4951
this.actions = new Map<ItemId, GameAction>();
52+
this.quizzes = new Map<ItemId, Quiz>();
5053

5154
this.gameStartActions = [];
5255
this.checkpointCompleteActions = [];
@@ -120,6 +123,10 @@ class GameMap {
120123
return this.actions;
121124
}
122125

126+
public getQuizMap(): Map<ItemId, Quiz> {
127+
return this.quizzes;
128+
}
129+
123130
public getSoundAssets(): SoundAsset[] {
124131
return this.soundAssets;
125132
}

src/features/game/location/GameMapTypes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,6 @@ export enum GameItemType {
4848
characters = 'characters',
4949
actions = 'actions',
5050
bgmKey = 'bgmKey',
51-
collectibles = 'collectibles'
51+
collectibles = 'collectibles',
52+
quizzes = 'quizzes'
5253
}

src/features/game/parser/ActionParser.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,11 @@ export default class ActionParser {
185185
case GameActionType.Delay:
186186
actionParamObj.duration = parseInt(actionParams[0]) * 1000;
187187
break;
188+
189+
case GameActionType.ShowQuiz:
190+
actionParamObj.id = actionParams[0];
191+
Parser.validator.assertItemType(GameItemType.quizzes, actionParams[0], actionType);
192+
break;
188193
}
189194

190195
const actionId = Parser.generateActionId();

0 commit comments

Comments
 (0)