Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
09318cc
Changed GameManager, GameActionManager, and GameMapTypes
ItsByt Feb 13, 2026
14a1f30
Comments
ItsByt Feb 15, 2026
f45145b
Removed noSave
ItsByt Feb 15, 2026
81dd27a
removed all noSave
ItsByt Feb 15, 2026
3134b9a
changed
ItsByt Feb 15, 2026
50e5257
test upload
ItsByt Mar 25, 2026
213938c
Skip Button Features
ItsByt Mar 31, 2026
929a420
Fixed some minor bugs
ItsByt Apr 3, 2026
a64003a
Fixed skip logic and changed peekNextLine
ItsByt Apr 5, 2026
58bd51f
Merge branch 'master' into Personal
ItsByt Apr 6, 2026
1340911
Re-added finishTypeWriting
ItsByt Apr 6, 2026
567542c
Fix merge conflict when Merge branch 'Personal' of https://github.com…
ItsByt Apr 6, 2026
9843347
Testin lint
ItsByt Apr 6, 2026
4f6e333
Merge branch 'master' into Personal
ItsByt Apr 6, 2026
a977e08
change yarn
chenyuzhen2007-source Apr 6, 2026
1d94b83
Fixed skipConfirm to Settings
ItsByt Apr 6, 2026
fb2e1e3
Merge branch 'Personal' of https://github.com/ItsByt/frontend into Pe…
ItsByt Apr 6, 2026
7be35ec
Format Settings.ts
chenyuzhen2007-source Apr 6, 2026
e092ef5
Merge branch 'master' into Personal
martin-henz Apr 8, 2026
f1686b1
Merge branch 'master' into Personal
martin-henz Apr 8, 2026
439c647
Merge branch 'master' into Personal
sayomaki Apr 10, 2026
9fb07b1
revert changes to yarn.lock
sayomaki Apr 10, 2026
8112de2
Change server port from 8080 to 8000
ItsByt Apr 12, 2026
e6a4fd3
Changed settings, constants and a bug fix for skip
ItsByt Apr 12, 2026
8fd238b
Changed settings, constants and a bug fix for skip
ItsByt Apr 12, 2026
7c6e38f
Changed settings, constants and a bug fix for skip
ItsByt Apr 12, 2026
929cd4d
Merge branch 'master' into Personal
sayomaki Apr 13, 2026
26ecbd2
Added 'Skip Confirm' option to settings header
ItsByt Apr 14, 2026
e1ba8cd
Add skip confirm radio buttons to settings layer
ItsByt Apr 14, 2026
b865ff2
Fixed skipRemainingDialogue
ItsByt Apr 14, 2026
1e0790b
Merge branch 'master' into Personal
sayomaki Apr 16, 2026
9181619
Merge branch 'master' into Personal
martin-henz Apr 17, 2026
2461297
Merge branch 'master' into Personal
martin-henz Apr 21, 2026
ddfa7b4
Merge branch 'master' into Personal
sayomaki May 7, 2026
5f32262
Use createButton instead of an additional function, and update asset …
sayomaki May 7, 2026
d7fa2d3
Remove skip icon from public assets
sayomaki May 7, 2026
2092101
Merge branch 'master' into Personal
RichDom2185 May 7, 2026
6497ba4
Add back button display size
sayomaki May 8, 2026
5755894
Make skip transition slower and more natural
sayomaki May 8, 2026
94850ed
Fix skip button to not show for any upcoming line that requires user …
sayomaki May 8, 2026
9b688a9
Improve skip transition to feel more natural
sayomaki May 8, 2026
e87c30b
Merge branch 'master' into Personal
RichDom2185 May 13, 2026
9c842a4
Merge branch 'master' of https://github.com/source-academy/frontend i…
RichDom2185 May 13, 2026
204f4b6
Reformat files post-merge
RichDom2185 May 13, 2026
4a34c8c
Improve comment formatting
RichDom2185 May 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/assets/skip-icon.png
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be preferred to use the S3 for the skip icon instead, just like any other game asset. It should not be put into frontend.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, could someone help me move this asset into the S3? Thank you!

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion rsbuild.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default defineConfig({
})
],
server: {
port: 8000
port: 8080
Comment thread
sayomaki marked this conversation as resolved.
Outdated
},
tools: {
// TODO: See if still needed
Expand Down
8 changes: 8 additions & 0 deletions src/features/game/dialogue/GameDialogueGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,12 @@ export default class DialogueGenerator {
this.currPart = goto;
this.currLineNum = 0;
}

public peekNextLine(): DialogueLine | null {
const lines = this.dialogueContent.get(this.currPart);
if (!lines || !lines[this.currLineNum]) {
return null;
}
return lines[this.currLineNum];
}
}
200 changes: 189 additions & 11 deletions src/features/game/dialogue/GameDialogueManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import SoundAssets from '../assets/SoundAssets';
import { screenSize } from '../commons/CommonConstants';
import { ItemId } from '../commons/CommonTypes';
import { promptWithChoices } from '../effects/Prompt';
import { keyboardShortcuts } from '../input/GameInputConstants';
Expand All @@ -25,6 +26,12 @@ export default class DialogueManager {
GameGlobalAPI.getInstance().getGameManager()
);

private skipButton?: Phaser.GameObjects.Image;
private isSkipping: boolean = false;
private nextLineResolve?: (value: void | PromiseLike<void>) => void;
private isPrompting: boolean = false;
private isDialoguePromptActive: boolean = false;

/**
* @param dialogueId the dialogue Id of the dialogue you want to play
*
Expand All @@ -38,39 +45,191 @@ export default class DialogueManager {
this.dialogueGenerator = new DialogueGenerator(dialogue.content);
this.speakerRenderer = new DialogueSpeakerRenderer();

GameGlobalAPI.getInstance().addToLayer(
Layer.Dialogue,
this.dialogueRenderer.getDialogueContainer()
);
const dialogueContainer = this.dialogueRenderer.getDialogueContainer();
this.createSkipButton(dialogueContainer);

GameGlobalAPI.getInstance().addToLayer(Layer.Dialogue, dialogueContainer);

GameGlobalAPI.getInstance().fadeInLayer(Layer.Dialogue);
await new Promise(resolve => this.playWholeDialogue(resolve as () => void));

if (this.skipButton) {
this.skipButton.destroy();
this.skipButton = undefined;
}

this.getDialogueRenderer().destroy();
this.getSpeakerRenderer().changeSpeakerTo(null);
}

private async playWholeDialogue(resolve: () => void) {
await this.showNextLine(resolve);
// add keyboard listener for dialogue box
this.nextLineResolve = () => this.showNextLine(resolve);
Comment thread
ItsByt marked this conversation as resolved.

this.getInputManager().registerKeyboardListener(keyboardShortcuts.Next, 'up', async () => {
// show the next line if dashboard or escape menu are not displayed
if (
!GameGlobalAPI.getInstance().getGameManager().getPhaseManager().isCurrentPhaseTerminal()
) {
await this.showNextLine(resolve);
if (!this.isSkipping && !this.isPrompting) {
await this.showNextLine(resolve);
}
}
});

this.getInputManager().registerKeyboardListener(
keyboardShortcuts.SkipDialogue,
'up',
async () => {
await this.triggerSkip();
}
);

// Dialogue Box Mouse Click
this.getDialogueRenderer()
.getDialogueBox()
.on(Phaser.Input.Events.GAMEOBJECT_POINTER_UP, async () => {
await this.showNextLine(resolve);
if (!this.isSkipping && !this.isPrompting) {
await this.showNextLine(resolve);
}
});
}

public async showNextLine(resolve: () => void) {
private async triggerSkip() {
if (this.isPrompting || this.isSkipping || this.isDialoguePromptActive) return;

const gameManager = GameGlobalAPI.getInstance().getGameManager();
const phaseManager = gameManager.getPhaseManager();

if (phaseManager.isCurrentPhaseTerminal()) return;

const settings = SourceAcademyGame.getInstance().getSaveManager().getSettings();
const requiresConfirm = settings.skipConfirm !== false;

if (requiresConfirm) {
this.isPrompting = true;

if (this.skipButton) {
this.skipButton.setVisible(false);
this.skipButton.disableInteractive();
}

this.getInputManager().enableKeyboardInput(false);

const dialogueBox = this.getDialogueRenderer().getDialogueBox();
GameGlobalAPI.getInstance().enableSprite(dialogueBox, false);

const response = await promptWithChoices(
gameManager,
'Skip remaining dialogue?',
['Yes', 'No'],
Layer.Dialogue
);

this.getInputManager().enableKeyboardInput(true);
GameGlobalAPI.getInstance().enableSprite(dialogueBox, true);

if (this.skipButton) {
this.skipButton.setVisible(true);
this.skipButton.setInteractive({ useHandCursor: true });
}

this.isPrompting = false;

if (response === 0) {
await this.skipRemainingDialogue();
}
} else {
await this.skipRemainingDialogue();
}
}

private createSkipButton(dialogueContainer: Phaser.GameObjects.Container) {
const gameManager = GameGlobalAPI.getInstance().getGameManager();

this.skipButton = new Phaser.GameObjects.Image(
gameManager,
screenSize.x - 62.5,
screenSize.y * 0.73,
Comment thread
sayomaki marked this conversation as resolved.
Outdated
'skip-icon'
).setInteractive({ useHandCursor: true });

this.skipButton.setDisplaySize(45, 45);
Comment thread
sayomaki marked this conversation as resolved.
Outdated

this.skipButton.on(Phaser.Input.Events.GAMEOBJECT_POINTER_UP, async () => {
await this.triggerSkip();
});

dialogueContainer.add(this.skipButton);
}

/**
* Skips all remaining dialogue until a prompt (choice) is encountered.
* Does not skip prompts that require user input.
*/
private async skipRemainingDialogue() {
if (this.isSkipping) return; // Prevent multiple skip calls
this.isSkipping = true;

// Hide and disable button while skipping
if (this.skipButton) {
this.skipButton.setVisible(false);
this.skipButton.disableInteractive();
}

// Plays the sound effect exactly once when skip starts
GameGlobalAPI.getInstance().playSound(SoundAssets.dialogueAdvance.key);

try {
// Keep advancing the dialogue until we hit a stopping point
while (this.isSkipping) {
if (this.dialogueRenderer) {
this.dialogueRenderer.finishTypewriting();
}

const nextLine = this.getDialogueGenerator().peekNextLine();

if (nextLine && nextLine.prompt) {
this.isSkipping = false;
break;
}

if (this.nextLineResolve) {
await this.nextLineResolve();
}

if (!nextLine || !nextLine.line) {
this.isSkipping = false;
break;
}
Comment thread
ItsByt marked this conversation as resolved.
Comment thread
ItsByt marked this conversation as resolved.

// Small delay to prevent freezing
await new Promise(resolve => setTimeout(resolve, 50));
Comment thread
ItsByt marked this conversation as resolved.
Outdated
}
} finally {
this.isSkipping = false;

if (
this.skipButton &&
!this.isDialoguePromptActive &&
this.getDialogueGenerator().peekNextLine() !== null
) {
this.skipButton.setVisible(true);
this.skipButton.setInteractive({ useHandCursor: true });
}
}
}

public async showNextLine(resolve: () => void) {
// Only play sound if we are not skipping, to avoid spamming sounds when skipping
if (!this.isSkipping) {
GameGlobalAPI.getInstance().playSound(SoundAssets.dialogueAdvance.key);
}

const { line, speakerDetail, actionIds, prompt } =
await this.getDialogueGenerator().generateNextLine();

const lineWithQuizScores = this.makeLineWithQuizScores(line);
const lineWithName = lineWithQuizScores.replace('{name}', this.getUsername());
this.getDialogueRenderer().changeText(lineWithName);
Expand All @@ -84,25 +243,44 @@ export default class DialogueManager {
this.getInputManager().enableKeyboardInput(false);

if (prompt) {
// disable keyboard input to prevent continue dialogue
// Prevent skipping, hide the skip button and prevent the usage of "s" keyboard shortcut
Comment thread
ItsByt marked this conversation as resolved.
this.isDialoguePromptActive = true;
if (this.skipButton) this.skipButton.setVisible(false);

this.getInputManager().enableKeyboardInput(false);
const response = await promptWithChoices(
GameGlobalAPI.getInstance().getGameManager(),
prompt.promptTitle,
prompt.choices.map(choice => choice[0])
);

this.getInputManager().enableKeyboardInput(true);
this.getDialogueGenerator().updateCurrPart(prompt.choices[response][1]);

if (this.skipButton) this.skipButton.setVisible(true);
this.isDialoguePromptActive = false;
}

await GameGlobalAPI.getInstance().processGameActionsInSamePhase(actionIds);
GameGlobalAPI.getInstance().enableSprite(this.getDialogueRenderer().getDialogueBox(), true);
this.getInputManager().enableKeyboardInput(true);

if (!line) {
// clear keyboard listeners when dialogue ends
this.getInputManager().clearKeyboardListeners([keyboardShortcuts.Next]);
//Permanently hide the skip button when there is no more dialogue
if (this.skipButton) {
this.skipButton.setVisible(false);
}

// Prevents skipping by using the "s" keyboard shortcut
this.getInputManager().clearKeyboardListeners([
keyboardShortcuts.Next,
keyboardShortcuts.SkipDialogue
]);
resolve();
} else if (!this.isSkipping && !this.isDialoguePromptActive) {
if (this.skipButton) {
this.skipButton.setVisible(true);
this.skipButton.setInteractive({ useHandCursor: true });
}
}
}

Expand Down
12 changes: 11 additions & 1 deletion src/features/game/dialogue/GameDialogueRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ class DialogueRenderer {
*/
public destroy() {
const gameManager = GameGlobalAPI.getInstance().getGameManager();
this.typewriter.clearTyping();

if (this.typewriter && this.typewriter.clearTyping) {
this.typewriter.clearTyping();
}

this.blinkingDiamond.clearBlink();
this.getDialogueBox().off(Phaser.Input.Events.GAMEOBJECT_POINTER_UP);
fadeAndDestroy(gameManager, this.getDialogueContainer());
Expand Down Expand Up @@ -91,6 +95,12 @@ class DialogueRenderer {
public changeText(message: string) {
this.typewriter.changeLine(message);
}

public finishTypewriting() {
if (this.typewriter && this.typewriter.finishTyping) {
this.typewriter.finishTyping();
}
}
Comment thread
ItsByt marked this conversation as resolved.
}

export default DialogueRenderer;
20 changes: 17 additions & 3 deletions src/features/game/effects/Prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ const promptOptStyle: BitmapFontStyle = {
export async function promptWithChoices(
scene: Phaser.Scene,
text: string,
choices: string[]
choices: string[],
targetLayer: Layer = Layer.UI
): Promise<number> {
const promptContainer = new Phaser.GameObjects.Container(scene, 0, 0);

Expand Down Expand Up @@ -87,7 +88,10 @@ export async function promptWithChoices(
ySpacing: PromptConstants.yInterval
});

GameGlobalAPI.getInstance().addToLayer(Layer.UI, promptContainer);
GameGlobalAPI.getInstance().addToLayer(targetLayer, promptContainer);

//Used to prevent spamming the confirm for the skip button using shortcut key
let isResolved = false;

const activatePromptContainer: Promise<number> = new Promise(resolve => {
promptContainer.add(
Expand All @@ -98,7 +102,17 @@ export async function promptWithChoices(
textConfig: PromptConstants.textConfig,
bitMapTextStyle: promptOptStyle,
onUp: () => {
promptContainer.destroy();
//Prevents the confirm prompt from showing up multiple times if someone tries to spam clicks it
if (isResolved) return;

const phaseManager = GameGlobalAPI.getInstance().getGameManager().getPhaseManager();

if (phaseManager.isCurrentPhaseTerminal()) {
return;
}
Comment thread
ItsByt marked this conversation as resolved.
Comment thread
ItsByt marked this conversation as resolved.

isResolved = true;

resolve(index);
}
}).setPosition(
Expand Down
11 changes: 10 additions & 1 deletion src/features/game/effects/Typewriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,16 @@ export function Typewriter(
}, typeWriterInterval);
};

return { container: textSprite, changeLine, clearTyping };
const finishTyping = () => {
clearTyping();

if (line) {
textSprite.text = line;
charPointer = line.length;
}
};

return { container: textSprite, changeLine, clearTyping, finishTyping };
}

export default Typewriter;
5 changes: 3 additions & 2 deletions src/features/game/escape/GameEscapeConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ export const optTextStyle: BitmapFontStyle = {
const EscapeConstants = {
button: { y: screenSize.y * 0.15 },
escapeOptTextConfig: { x: 0, y: 0, oriX: 0.37, oriY: 0.75 },
settings: { yOffset: -screenCenter.y * 0.1, ySpace: screenSize.y * 0.3 },
settings: { yOffset: -screenCenter.y * 0.1, ySpace: screenSize.y * 0.2 },
settingsTextConfig: { x: screenSize.x * 0.38, y: -screenCenter.y * 0.1, oriX: 0.0, oriY: 0.5 },
radioButtons: { xSpace: screenSize.x * 0.2 },
radioChoiceTextConfig: { x: 0, y: -45, oriX: 0.5, oriY: 0.25 },
volOpt: { x: screenSize.x * 0.05 }
volOpt: { x: screenSize.x * 0.05 },
skipConfirmOpts: ['ON', 'OFF']
};

export default EscapeConstants;
Loading
Loading