This commit is contained in:
Wlowscha 2025-08-19 00:35:02 +08:00 committed by GitHub
commit b50aa79236
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 186 additions and 228 deletions

View File

@ -8,6 +8,7 @@ import { addTextObject } from "#ui/text";
import { addWindow } from "#ui/ui-theme";
import { fixedInt, getLocalizedSpriteKey } from "#utils/common";
import i18next from "i18next";
import ScrollingText from "./scrolling-text";
export interface MoveInfoOverlaySettings {
/**
@ -35,16 +36,11 @@ export interface MoveInfoOverlaySettings {
const EFF_HEIGHT = 48;
const EFF_WIDTH = 82;
const DESC_HEIGHT = 48;
const BORDER = 8;
const GLOBAL_SCALE = 6;
export class MoveInfoOverlay extends Phaser.GameObjects.Container implements InfoToggle {
public active = false;
private move: Move;
private desc: Phaser.GameObjects.Text;
private descScroll: Phaser.Tweens.Tween | null = null;
private desc: ScrollingText;
private val: Phaser.GameObjects.Container;
private pp: Phaser.GameObjects.Text;
@ -52,7 +48,6 @@ export class MoveInfoOverlay extends Phaser.GameObjects.Container implements Inf
private acc: Phaser.GameObjects.Text;
private typ: Phaser.GameObjects.Sprite;
private cat: Phaser.GameObjects.Sprite;
private descBg: Phaser.GameObjects.NineSlice;
private options: MoveInfoOverlaySettings;
@ -64,55 +59,26 @@ export class MoveInfoOverlay extends Phaser.GameObjects.Container implements Inf
this.setScale(1);
this.options = options || {};
const descBoxX = options?.onSide && !options?.right ? EFF_WIDTH : 0;
const descBoxY = options?.top ? EFF_HEIGHT : 0;
const width = options?.width || MoveInfoOverlay.getWidth();
const descBoxWidth = width - (options?.onSide ? EFF_WIDTH : 0);
const descBoxHeight = DESC_HEIGHT;
// prepare the description box
const width = options?.width || MoveInfoOverlay.getWidth(); // we always want this to be half a window wide
this.descBg = addWindow(
options?.onSide && !options?.right ? EFF_WIDTH : 0,
options?.top ? EFF_HEIGHT : 0,
width - (options?.onSide ? EFF_WIDTH : 0),
DESC_HEIGHT,
);
this.descBg.setOrigin(0, 0);
this.add(this.descBg);
// set up the description; wordWrap uses true pixels, unaffected by any scaling, while other values are affected
this.desc = addTextObject(
(options?.onSide && !options?.right ? EFF_WIDTH : 0) + BORDER,
(options?.top ? EFF_HEIGHT : 0) + BORDER - 2,
"",
this.desc = new ScrollingText(
globalScene,
descBoxX,
descBoxY,
descBoxWidth,
descBoxHeight,
3, // maxLineCount
"", // initial content
TextStyle.BATTLE_INFO,
{
wordWrap: {
width: (width - (BORDER - 2) * 2 - (options?.onSide ? EFF_WIDTH : 0)) * GLOBAL_SCALE,
},
},
!options?.hideBg,
);
// limit the text rendering, required for scrolling later on
const maskPointOrigin = {
x: options?.x || 0,
y: options?.y || 0,
};
if (maskPointOrigin.x < 0) {
maskPointOrigin.x += globalScene.scaledCanvas.width;
}
if (maskPointOrigin.y < 0) {
maskPointOrigin.y += globalScene.scaledCanvas.height;
}
const moveDescriptionTextMaskRect = globalScene.make.graphics();
moveDescriptionTextMaskRect.fillStyle(0xff0000);
moveDescriptionTextMaskRect.fillRect(
maskPointOrigin.x + ((options?.onSide && !options?.right ? EFF_WIDTH : 0) + BORDER),
maskPointOrigin.y + ((options?.top ? EFF_HEIGHT : 0) + BORDER - 2),
width - ((options?.onSide ? EFF_WIDTH : 0) - BORDER * 2),
DESC_HEIGHT - (BORDER - 2) * 2,
);
moveDescriptionTextMaskRect.setScale(6);
const moveDescriptionTextMask = this.createGeometryMask(moveDescriptionTextMaskRect);
this.desc.createMask(globalScene, this.x + this.desc.x, this.y + this.desc.y);
this.add(this.desc);
this.desc.setMask(moveDescriptionTextMask);
// prepare the effect box
this.val = new Phaser.GameObjects.Container(
@ -164,10 +130,6 @@ export class MoveInfoOverlay extends Phaser.GameObjects.Container implements Inf
this.val.setVisible(false);
}
if (options?.hideBg) {
this.descBg.setVisible(false);
}
// hide this component for now
this.setVisible(false);
}
@ -177,35 +139,16 @@ export class MoveInfoOverlay extends Phaser.GameObjects.Container implements Inf
if (!globalScene.enableMoveInfo) {
return false; // move infos have been disabled // TODO:: is `false` correct? i used to be `undeefined`
}
this.move = move;
this.pow.setText(move.power >= 0 ? move.power.toString() : "---");
this.acc.setText(move.accuracy >= 0 ? move.accuracy.toString() : "---");
this.pp.setText(move.pp >= 0 ? move.pp.toString() : "---");
this.typ.setTexture(getLocalizedSpriteKey("types"), PokemonType[move.type].toLowerCase());
this.cat.setFrame(MoveCategory[move.category].toLowerCase());
this.desc.setText(move?.effect || "");
this.desc.text.setText(move?.effect || "");
// stop previous scrolling effects and reset y position
if (this.descScroll) {
this.descScroll.remove();
this.descScroll = null;
this.desc.y = (this.options?.top ? EFF_HEIGHT : 0) + BORDER - 2;
}
// determine if we need to add new scrolling effects
const moveDescriptionLineCount = Math.floor((this.desc.displayHeight * (96 / 72)) / 14.83);
if (moveDescriptionLineCount > 3) {
// generate scrolling effects
this.descScroll = globalScene.tweens.add({
targets: this.desc,
delay: fixedInt(2000),
loop: -1,
hold: fixedInt(2000),
duration: fixedInt((moveDescriptionLineCount - 3) * 2000),
y: `-=${14.83 * (72 / 96) * (moveDescriptionLineCount - 3)}`,
});
}
this.desc.activate();
if (!this.options.delayVisibility) {
this.setVisible(true);
@ -224,7 +167,7 @@ export class MoveInfoOverlay extends Phaser.GameObjects.Container implements Inf
this.setVisible(true);
}
globalScene.tweens.add({
targets: this.desc,
targets: this.desc.text,
duration: fixedInt(125),
ease: "Sine.easeInOut",
alpha: visible ? 1 : 0,

View File

@ -1,9 +1,8 @@
import type { InfoToggle } from "#app/battle-scene";
import { globalScene } from "#app/global-scene";
import { TextStyle } from "#enums/text-style";
import { addTextObject } from "#ui/text";
import { addWindow } from "#ui/ui-theme";
import { fixedInt } from "#utils/common";
import ScrollingText from "./scrolling-text";
export interface PokedexInfoOverlaySettings {
delayVisibility?: boolean; // if true, showing the overlay will only set it to active and populate the fields and the handler using this field has to manually call setVisible later.
@ -14,17 +13,14 @@ export interface PokedexInfoOverlaySettings {
width?: number;
/** Determines whether to display the small secondary box */
hideEffectBox?: boolean;
hideBg?: boolean;
}
const DESC_HEIGHT = 48;
const BORDER = 8;
const GLOBAL_SCALE = 6;
export class PokedexInfoOverlay extends Phaser.GameObjects.Container implements InfoToggle {
public active = false;
private desc: Phaser.GameObjects.Text;
private desc: ScrollingText;
private descScroll: Phaser.Tweens.Tween | null = null;
private descBg: Phaser.GameObjects.NineSlice;
@ -35,52 +31,32 @@ export class PokedexInfoOverlay extends Phaser.GameObjects.Container implements
private maskPointOriginX: number;
private maskPointOriginY: number;
public width: number;
constructor(options?: PokedexInfoOverlaySettings) {
super(globalScene, options?.x, options?.y);
this.setScale(1);
this.options = options || {};
const descBoxX = 0;
const descBoxY = 0;
const width = options?.width || PokedexInfoOverlay.getWidth();
const descBoxWidth = width;
const descBoxHeight = DESC_HEIGHT;
// prepare the description box
this.width = options?.width || PokedexInfoOverlay.getWidth(); // we always want this to be half a window wide
this.descBg = addWindow(0, 0, this.width, DESC_HEIGHT);
this.descBg.setOrigin(0, 0);
this.add(this.descBg);
// set up the description; wordWrap uses true pixels, unaffected by any scaling, while other values are affected
this.desc = addTextObject(BORDER, BORDER - 2, "", TextStyle.BATTLE_INFO, {
wordWrap: { width: (this.width - (BORDER - 2) * 2) * GLOBAL_SCALE },
});
// limit the text rendering, required for scrolling later on
this.maskPointOriginX = options?.x || 0;
this.maskPointOriginY = options?.y || 0;
if (this.maskPointOriginX < 0) {
this.maskPointOriginX += globalScene.scaledCanvas.width;
}
if (this.maskPointOriginY < 0) {
this.maskPointOriginY += globalScene.scaledCanvas.height;
}
this.textMaskRect = globalScene.make.graphics();
this.textMaskRect.fillStyle(0xff0000);
this.textMaskRect.fillRect(
this.maskPointOriginX + BORDER,
this.maskPointOriginY + (BORDER - 2),
this.width - BORDER * 2,
DESC_HEIGHT - (BORDER - 2) * 2,
this.desc = new ScrollingText(
globalScene,
descBoxX,
descBoxY,
descBoxWidth,
descBoxHeight,
3, // maxLineCount
"", // initial content
TextStyle.BATTLE_INFO,
true,
);
this.textMaskRect.setScale(6);
const textMask = this.createGeometryMask(this.textMaskRect);
this.desc.createMask(globalScene, this.x + this.desc.x, this.y + this.desc.y);
this.add(this.desc);
this.desc.setMask(textMask);
if (options?.hideBg) {
this.descBg.setVisible(false);
}
// hide this component for now
this.setVisible(false);
@ -92,45 +68,10 @@ export class PokedexInfoOverlay extends Phaser.GameObjects.Container implements
return false; // move infos have been disabled // TODO:: is `false` correct? i used to be `undeefined`
}
this.desc.setText(text ?? "");
this.desc.text.setText(text ?? "");
// stop previous scrolling effects and reset y position
if (this.descScroll) {
this.descScroll.remove();
this.descScroll = null;
this.desc.y = BORDER - 2;
}
// determine if we need to add new scrolling effects
const lineCount = Math.floor((this.desc.displayHeight * (96 / 72)) / 14.83);
const newHeight = lineCount >= 3 ? 48 : lineCount === 2 ? 36 : 24;
this.textMaskRect.clear();
this.textMaskRect.fillStyle(0xff0000);
this.textMaskRect.fillRect(
this.maskPointOriginX + BORDER,
this.maskPointOriginY + (BORDER - 2) + (48 - newHeight),
this.width - BORDER * 2,
newHeight - (BORDER - 2) * 2,
);
const updatedMask = this.createGeometryMask(this.textMaskRect);
this.desc.setMask(updatedMask);
this.descBg.setSize(this.descBg.width, newHeight);
this.descBg.setY(48 - newHeight);
this.desc.setY(BORDER - 2 + (48 - newHeight));
if (lineCount > 3) {
// generate scrolling effects
this.descScroll = globalScene.tweens.add({
targets: this.desc,
delay: fixedInt(2000),
loop: -1,
hold: fixedInt(2000),
duration: fixedInt((lineCount - 3) * 2000),
y: `-=${14.83 * (72 / 96) * (lineCount - 3)}`,
});
}
this.desc.activate();
if (!this.options.delayVisibility) {
this.setVisible(true);

105
src/ui/scrolling-text.ts Normal file
View File

@ -0,0 +1,105 @@
import { globalScene } from "#app/global-scene";
import { fixedInt } from "#app/utils/common";
import type { TextStyle } from "#enums/text-style";
import i18next from "i18next";
import { addTextObject } from "./text";
import { addWindow } from "./ui-theme";
/*
This takes various coordinates:
- The x and y coordinates relative to the parent container, this is typical behavior for Phaser.GameObjects.Container.
- The width and height of the box; these are needed to create the background.
The mask is not created right away (although this is possible in principle). Instead, we have a separate function,
which takes as input the _global_ coordinates of scrolling text object. This is necessary to correctly position the mask in the scene.
*/
const BORDER = 8;
const SCALE_PROPERTY = 96 / 72 / 14.83;
export default class ScrollingText extends Phaser.GameObjects.Container {
private descBg: Phaser.GameObjects.NineSlice;
public text: Phaser.GameObjects.Text;
private descScroll: Phaser.Tweens.Tween | null = null;
private maxLineCount: number;
private offsetX: number;
private offsetY: number;
constructor(
scene: Phaser.Scene,
x: number,
y: number,
width: number,
height: number,
maxLineCount: number,
content: string,
style: TextStyle,
hasBackground = false,
) {
super(scene, x, y);
this.offsetX = hasBackground ? BORDER : 0;
this.offsetY = hasBackground ? BORDER - 2 : 0;
// Adding the background
this.descBg = addWindow(0, 0, width, height);
this.descBg.setOrigin(0, 0);
this.descBg.setVisible(hasBackground);
this.add(this.descBg);
// Adding the text element
const wrapWidth = (width - (this.offsetX - 2) * 2) * 6;
this.text = addTextObject(this.offsetX, this.offsetY, content, style, {
wordWrap: {
width: wrapWidth,
},
});
this.maxLineCount = maxLineCount;
// TODO: change this based on which text is being used, etc
this.text.setLineSpacing(i18next.resolvedLanguage === "ja" ? 25 : 5);
this.add(this.text);
}
createMask(scene: Phaser.Scene, globalX: number, globalY: number) {
// Adding the mask for the scrolling effect
const globalMaskX = globalX + this.offsetX;
const globalMaskY = globalY + this.offsetY;
const visibleWidth = this.descBg.width - (this.offsetX - 2) * 2;
const visibleHeight = this.descBg.height - this.offsetY * 2;
const maskGraphics = scene.make.graphics({ x: 0, y: 0 });
maskGraphics.fillRect(globalMaskX, globalMaskY, visibleWidth, visibleHeight);
maskGraphics.setScale(6);
scene.add.existing(maskGraphics);
const mask = this.createGeometryMask(maskGraphics);
this.text.setMask(mask);
}
activate() {
// stop previous scrolling effects and reset y position
if (this.descScroll) {
this.descScroll.remove();
this.descScroll = null;
this.text.y = this.offsetY;
}
// determine if we need to add new scrolling effects
// TODO: The scale property may need to be adjusted based on the height of the font
const lineCount = Math.floor(this.text.displayHeight * SCALE_PROPERTY);
if (lineCount > this.maxLineCount) {
// generate scrolling effects
this.descScroll = globalScene.tweens.add({
targets: this.text,
delay: fixedInt(2000),
loop: -1,
hold: fixedInt(2000),
duration: fixedInt((lineCount - this.maxLineCount) * 2000),
y: `-=${(lineCount - this.maxLineCount) / SCALE_PROPERTY}`,
});
}
}
}

View File

@ -40,6 +40,7 @@ import { getEnumValues } from "#utils/enums";
import { toCamelCase, toTitleCase } from "#utils/strings";
import { argbFromRgba } from "@material/material-color-utilities";
import i18next from "i18next";
import ScrollingText from "./scrolling-text";
enum Page {
PROFILE,
@ -61,7 +62,7 @@ interface abilityContainer {
/** The text object displaying the name of the ability */
nameText: Phaser.GameObjects.Text | null;
/** The text object displaying the description of the ability */
descriptionText: Phaser.GameObjects.Text | null;
description: ScrollingText | null;
}
export class SummaryUiHandler extends UiHandler {
@ -94,7 +95,7 @@ export class SummaryUiHandler extends UiHandler {
private passiveContainer: abilityContainer;
private summaryPageContainer: Phaser.GameObjects.Container;
private movesContainer: Phaser.GameObjects.Container;
private moveDescriptionText: Phaser.GameObjects.Text;
private moveDescription: ScrollingText;
private moveCursorObj: Phaser.GameObjects.Sprite | null;
private selectedMoveCursorObj: Phaser.GameObjects.Sprite | null;
private moveRowsContainer: Phaser.GameObjects.Container;
@ -583,12 +584,12 @@ export class SummaryUiHandler extends UiHandler {
} else if (this.cursor === Page.PROFILE && this.pokemon?.hasPassive()) {
// if we're on the PROFILE page and this pokemon has a passive unlocked..
// Since abilities are displayed by default, all we need to do is toggle visibility on all elements to show passives
this.abilityContainer.nameText?.setVisible(!this.abilityContainer.descriptionText?.visible);
this.abilityContainer.descriptionText?.setVisible(!this.abilityContainer.descriptionText.visible);
this.abilityContainer.nameText?.setVisible(!this.abilityContainer.description?.visible);
this.abilityContainer.description?.setVisible(!this.abilityContainer.description.visible);
this.abilityContainer.labelImage.setVisible(!this.abilityContainer.labelImage.visible);
this.passiveContainer.nameText?.setVisible(!this.passiveContainer.descriptionText?.visible);
this.passiveContainer.descriptionText?.setVisible(!this.passiveContainer.descriptionText.visible);
this.passiveContainer.nameText?.setVisible(!this.passiveContainer.description?.visible);
this.passiveContainer.description?.setVisible(!this.passiveContainer.description.visible);
this.passiveContainer.labelImage.setVisible(!this.passiveContainer.labelImage.visible);
} else if (this.cursor === Page.STATS) {
//Show IVs
@ -668,7 +669,6 @@ export class SummaryUiHandler extends UiHandler {
const selectedMove = this.getSelectedMove();
if (selectedMove) {
this.moveDescriptionText.setY(84);
this.movePowerText.setText(selectedMove.power >= 0 ? selectedMove.power.toString() : "---");
this.moveAccuracyText.setText(selectedMove.accuracy >= 0 ? selectedMove.accuracy.toString() : "---");
this.moveCategoryIcon.setFrame(MoveCategory[selectedMove.category].toLowerCase());
@ -677,24 +677,9 @@ export class SummaryUiHandler extends UiHandler {
this.hideMoveEffect();
}
this.moveDescriptionText.setText(selectedMove?.effect || "");
const moveDescriptionLineCount = Math.floor(this.moveDescriptionText.displayHeight / 14.83);
this.moveDescription.text.setText(selectedMove?.effect || "");
if (this.descriptionScrollTween) {
this.descriptionScrollTween.remove();
this.descriptionScrollTween = null;
}
if (moveDescriptionLineCount > 3) {
this.descriptionScrollTween = globalScene.tweens.add({
targets: this.moveDescriptionText,
delay: fixedInt(2000),
loop: -1,
hold: fixedInt(2000),
duration: fixedInt((moveDescriptionLineCount - 3) * 2000),
y: `-=${14.83 * (moveDescriptionLineCount - 3)}`,
});
}
this.moveDescription.activate();
if (!this.moveCursorObj) {
this.moveCursorObj = globalScene.add.sprite(-2, 0, "summary_moves_cursor", "highlight");
@ -888,7 +873,7 @@ export class SummaryUiHandler extends UiHandler {
labelImage: globalScene.add.image(0, 0, "summary_profile_ability"),
ability: this.pokemon?.getAbility(true)!, // TODO: is this bang correct?
nameText: null,
descriptionText: null,
description: null,
};
const allAbilityInfo = [this.abilityContainer]; // Creates an array to iterate through
@ -898,7 +883,7 @@ export class SummaryUiHandler extends UiHandler {
labelImage: globalScene.add.image(0, 0, "summary_profile_passive"),
ability: this.pokemon.getPassiveAbility(),
nameText: null,
descriptionText: null,
description: null,
};
allAbilityInfo.push(this.passiveContainer);
@ -924,42 +909,26 @@ export class SummaryUiHandler extends UiHandler {
abilityInfo.nameText.setOrigin(0, 1);
profileContainer.add(abilityInfo.nameText);
abilityInfo.descriptionText = addTextObject(7, 71, abilityInfo.ability?.description!, TextStyle.WINDOW_ALT, {
wordWrap: { width: 1224 },
}); // TODO: is this bang correct?
abilityInfo.descriptionText.setOrigin(0, 0);
profileContainer.add(abilityInfo.descriptionText);
abilityInfo.description = new ScrollingText(
globalScene,
7,
71,
206,
31,
2, // maxLineCount
abilityInfo.ability?.description!, // initial content
TextStyle.WINDOW_ALT,
);
abilityInfo.description.createMask(globalScene, 110, 90.5);
profileContainer.add(abilityInfo.description);
// Sets up the mask that hides the description text to give an illusion of scrolling
const descriptionTextMaskRect = globalScene.make.graphics({});
descriptionTextMaskRect.setScale(6);
descriptionTextMaskRect.fillStyle(0xffffff);
descriptionTextMaskRect.beginPath();
descriptionTextMaskRect.fillRect(110, 90.5, 206, 31);
const abilityDescriptionTextMask = descriptionTextMaskRect.createGeometryMask();
abilityInfo.descriptionText.setMask(abilityDescriptionTextMask);
const abilityDescriptionLineCount = Math.floor(abilityInfo.descriptionText.displayHeight / 14.83);
// Animates the description text moving upwards
if (abilityDescriptionLineCount > 2) {
abilityInfo.descriptionText.setY(69);
this.descriptionScrollTween = globalScene.tweens.add({
targets: abilityInfo.descriptionText,
delay: fixedInt(2000),
loop: -1,
hold: fixedInt(2000),
duration: fixedInt((abilityDescriptionLineCount - 2) * 2000),
y: `-=${14.83 * (abilityDescriptionLineCount - 2)}`,
});
}
abilityInfo.description.activate();
});
// Turn off visibility of passive info by default
this.passiveContainer?.labelImage.setVisible(false);
this.passiveContainer?.nameText?.setVisible(false);
this.passiveContainer?.descriptionText?.setVisible(false);
this.passiveContainer?.description?.setVisible(false);
const closeFragment = getBBCodeFrag("", TextStyle.WINDOW_ALT);
const rawNature = toCamelCase(Nature[this.pokemon?.getNature()!]); // TODO: is this bang correct?
@ -1182,18 +1151,18 @@ export class SummaryUiHandler extends UiHandler {
moveRowContainer.add(ppText);
}
this.moveDescriptionText = addTextObject(2, 84, "", TextStyle.WINDOW_ALT, { wordWrap: { width: 1212 } });
this.movesContainer.add(this.moveDescriptionText);
const moveDescriptionTextMaskRect = globalScene.make.graphics({});
moveDescriptionTextMaskRect.setScale(6);
moveDescriptionTextMaskRect.fillStyle(0xffffff);
moveDescriptionTextMaskRect.beginPath();
moveDescriptionTextMaskRect.fillRect(112, 130, 202, 46);
const moveDescriptionTextMask = moveDescriptionTextMaskRect.createGeometryMask();
this.moveDescriptionText.setMask(moveDescriptionTextMask);
this.moveDescription = new ScrollingText(
globalScene,
2,
84,
202,
46,
3, // maxLineCount
"", // initial content
TextStyle.WINDOW_ALT,
);
this.moveDescription.createMask(globalScene, 112, 130);
this.movesContainer.add(this.moveDescription);
break;
}
}
@ -1255,7 +1224,7 @@ export class SummaryUiHandler extends UiHandler {
this.moveSelect = false;
this.extraMoveRowContainer.setVisible(false);
this.moveDescriptionText.setText("");
this.moveDescription.text.setText("");
this.destroyBlinkCursor();
this.hideMoveEffect();