pokerogue/src/ui/message-ui-handler.ts
Matilde Simões a1a6b0dd5a
[Bug] Nicknames not properly sanitized (#5537)
* Fix #5082: Nicknames not properly sanitized
When a player changes the name of the pokemon
to one that uses one of the following combination
of letters: "@c{}", "@s{}", "@d{}", "@f{}" and "$"
the game shows the name of the pokemon incorrectly in a battle.
Changes made:
- on message-ui-handler.ts file I updated the "showTextInternal"
function to get the original name of the pokemon
or pokemons (in case it's a double battle) saving it in a list
named "pokename" and change it in the text for their
correspondent placeholder which is saved in the list "repname"
(e.g "#POKEMON1" for the first pokemon and "#POKEMON2" for the
second pokemon). After the text is properly modified because
of the special characters ("@c{}", "@s{}", "@d{}", "@f{}")
the name of the pokemons is replaced to it's original value.
- on message-phase.ts file I updated the "start" function to use a
similar approach but only change the pokemon name to it's original
form after the "pageIndex" (which checks the index of the "$")
is updated, so the text is cut properly.
- on ui.ts file I updated the "showtext" function to use same
approach of the previous files, ensuring that the pokemon names
were only replaced back to their original values after all text
processing on "$" was completed.

* Replace `let` with `const`

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
2025-03-19 22:01:33 +00:00

276 lines
8.5 KiB
TypeScript

import AwaitableUiHandler from "./awaitable-ui-handler";
import type { Mode } from "./ui";
import * as Utils from "../utils";
import { globalScene } from "#app/global-scene";
export default abstract class MessageUiHandler extends AwaitableUiHandler {
protected textTimer: Phaser.Time.TimerEvent | null;
protected textCallbackTimer: Phaser.Time.TimerEvent | null;
public pendingPrompt: boolean;
public message: Phaser.GameObjects.Text;
public prompt: Phaser.GameObjects.Sprite;
constructor(mode: Mode | null = null) {
super(mode);
this.pendingPrompt = false;
}
/**
* Add the sprite to be displayed at the end of messages with prompts
* @param container the container to add the sprite to
*/
initPromptSprite(container: Phaser.GameObjects.Container) {
if (!this.prompt) {
const promptSprite = globalScene.add.sprite(0, 0, "prompt");
promptSprite.setVisible(false);
promptSprite.setOrigin(0, 0);
this.prompt = promptSprite;
}
if (container) {
container.add(this.prompt);
}
}
showText(
text: string,
delay?: number | null,
callback?: Function | null,
callbackDelay?: number | null,
prompt?: boolean | null,
promptDelay?: number | null,
) {
this.showTextInternal(text, delay, callback, callbackDelay, prompt, promptDelay);
}
showDialogue(
text: string,
_name?: string,
delay?: number | null,
callback?: Function | null,
callbackDelay?: number | null,
prompt?: boolean | null,
promptDelay?: number | null,
) {
this.showTextInternal(text, delay, callback, callbackDelay, prompt, promptDelay);
}
private showTextInternal(
text: string,
delay?: number | null,
callback?: Function | null,
callbackDelay?: number | null,
prompt?: boolean | null,
promptDelay?: number | null,
) {
if (delay === null || delay === undefined) {
delay = 20;
}
// Pattern matching regex that checks for @c{}, @f{}, @s{}, and @f{} patterns within message text and parses them to their respective behaviors.
const charVarMap = new Map<number, string>();
const delayMap = new Map<number, number>();
const soundMap = new Map<number, string>();
const fadeMap = new Map<number, number>();
const actionPattern = /@(c|d|s|f)\{(.*?)\}/;
let actionMatch: RegExpExecArray | null;
const pokename: string[] = [];
const repname = [ "#POKEMON1", "#POKEMON2" ];
for (let p = 0; p < globalScene.getPlayerField().length; p++) {
pokename.push(globalScene.getPlayerField()[p].getNameToRender());
text = text.split(pokename[p]).join(repname[p]);
}
while ((actionMatch = actionPattern.exec(text))) {
switch (actionMatch[1]) {
case "c":
charVarMap.set(actionMatch.index, actionMatch[2]);
break;
case "d":
delayMap.set(actionMatch.index, Number.parseInt(actionMatch[2]));
break;
case "s":
soundMap.set(actionMatch.index, actionMatch[2]);
break;
case "f":
fadeMap.set(actionMatch.index, Number.parseInt(actionMatch[2]));
break;
}
text = text.slice(0, actionMatch.index) + text.slice(actionMatch.index + actionMatch[2].length + 4);
}
for (let p = 0; p < globalScene.getPlayerField().length; p++) {
text = text.split(repname[p]).join(pokename[p]);
}
if (text) {
// Predetermine overflow line breaks to avoid words breaking while displaying
const textWords = text.split(" ");
let lastLineCount = 1;
let newText = "";
for (let w = 0; w < textWords.length; w++) {
const nextWordText = newText ? `${newText} ${textWords[w]}` : textWords[w];
if (textWords[w].includes("\n")) {
newText = nextWordText;
lastLineCount++;
} else {
const lineCount = this.message.runWordWrap(nextWordText).split(/\n/g).length;
if (lineCount > lastLineCount) {
lastLineCount = lineCount;
newText = `${newText}\n${textWords[w]}`;
} else {
newText = nextWordText;
}
}
}
text = newText;
}
if (this.textTimer) {
this.textTimer.remove();
if (this.textCallbackTimer) {
this.textCallbackTimer.callback();
}
}
if (prompt) {
const originalCallback = callback;
callback = () => {
const showPrompt = () => this.showPrompt(originalCallback, callbackDelay);
if (promptDelay) {
globalScene.time.delayedCall(promptDelay, showPrompt);
} else {
showPrompt();
}
};
}
if (delay) {
this.clearText();
if (prompt) {
this.pendingPrompt = true;
}
this.textTimer = globalScene.time.addEvent({
delay: delay,
callback: () => {
const charIndex = text.length - this.textTimer?.repeatCount!; // TODO: is this bang correct?
const charVar = charVarMap.get(charIndex);
const charSound = soundMap.get(charIndex);
const charDelay = delayMap.get(charIndex);
const charFade = fadeMap.get(charIndex);
this.message.setText(text.slice(0, charIndex));
const advance = () => {
if (charVar) {
globalScene.charSprite.setVariant(charVar);
}
if (charSound) {
globalScene.playSound(charSound);
}
if (callback && !this.textTimer?.repeatCount) {
if (callbackDelay && !prompt) {
this.textCallbackTimer = globalScene.time.delayedCall(callbackDelay, () => {
if (this.textCallbackTimer) {
this.textCallbackTimer.destroy();
this.textCallbackTimer = null;
}
callback();
});
} else {
callback();
}
}
};
if (charDelay) {
this.textTimer!.paused = true; // TODO: is the bang correct?
globalScene.tweens.addCounter({
duration: Utils.getFrameMs(charDelay),
onComplete: () => {
this.textTimer!.paused = false; // TODO: is the bang correct?
advance();
},
});
} else if (charFade) {
this.textTimer!.paused = true;
globalScene.time.delayedCall(150, () => {
globalScene.ui.fadeOut(750).then(() => {
const delay = Utils.getFrameMs(charFade);
globalScene.time.delayedCall(delay, () => {
globalScene.ui.fadeIn(500).then(() => {
this.textTimer!.paused = false;
advance();
});
});
});
});
} else {
advance();
}
},
repeat: text.length,
});
} else {
this.message.setText(text);
if (prompt) {
this.pendingPrompt = true;
}
if (callback) {
callback();
}
}
}
showPrompt(callback?: Function | null, callbackDelay?: number | null) {
const wrappedTextLines = this.message.runWordWrap(this.message.text).split(/\n/g);
const textLinesCount = wrappedTextLines.length;
const lastTextLine = wrappedTextLines[wrappedTextLines.length - 1];
const lastLineTest = globalScene.add.text(0, 0, lastTextLine, {
font: "96px emerald",
});
lastLineTest.setScale(this.message.scale);
const lastLineWidth = lastLineTest.displayWidth;
lastLineTest.destroy();
if (this.prompt) {
this.prompt.setPosition(this.message.x + lastLineWidth + 2, this.message.y + (textLinesCount - 1) * 18 + 2);
this.prompt.play("prompt");
}
this.pendingPrompt = false;
this.awaitingActionInput = true;
this.onActionInput = () => {
if (this.prompt) {
this.prompt.anims.stop();
this.prompt.setVisible(false);
}
if (callback) {
if (callbackDelay) {
this.textCallbackTimer = globalScene.time.delayedCall(callbackDelay, () => {
if (this.textCallbackTimer) {
this.textCallbackTimer.destroy();
this.textCallbackTimer = null;
}
callback();
});
} else {
callback();
}
}
};
}
isTextAnimationInProgress() {
if (this.textTimer) {
return this.textTimer.repeatCount < this.textTimer.repeat;
}
return false;
}
clearText() {
this.message.setText("");
this.pendingPrompt = false;
}
clear() {
super.clear();
}
}