[Enhancement] [UI/UX] Add ability and passive tooltips to starter select screen (#4023)
* Add ability and passive tooltips to starter select screen * Remove explicit casts to BattleScene * Increase tooltip size, reverse y when necessary, and always show passive tooltip * Add ability name to tooltip title and persist tooltips between Pokemon * Use vi function mocks
This commit is contained in:
parent
9afab182e9
commit
9317093044
|
@ -52,9 +52,8 @@ export default class MockContainer implements MockGameObject {
|
||||||
/// Sets the position of this Game Object to be a relative position from the source Game Object.
|
/// Sets the position of this Game Object to be a relative position from the source Game Object.
|
||||||
}
|
}
|
||||||
|
|
||||||
setInteractive(hitArea?, callback?, dropZone?) {
|
setInteractive = vi.fn();
|
||||||
/// Sets the InteractiveObject to be a drop zone for a drag and drop operation.
|
|
||||||
}
|
|
||||||
setOrigin(x, y) {
|
setOrigin(x, y) {
|
||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { MockGameObject } from "../mockGameObject";
|
import { MockGameObject } from "../mockGameObject";
|
||||||
|
import { vi } from "vitest";
|
||||||
import Sprite = Phaser.GameObjects.Sprite;
|
import Sprite = Phaser.GameObjects.Sprite;
|
||||||
import Frame = Phaser.Textures.Frame;
|
import Frame = Phaser.Textures.Frame;
|
||||||
|
|
||||||
|
@ -101,9 +102,7 @@ export default class MockSprite implements MockGameObject {
|
||||||
return this.phaserSprite.stop();
|
return this.phaserSprite.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
setInteractive(hitArea, hitAreaCallback, dropZone) {
|
setInteractive = vi.fn();
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
on(event, callback, source) {
|
on(event, callback, source) {
|
||||||
return this.phaserSprite.on(event, callback, source);
|
return this.phaserSprite.on(event, callback, source);
|
||||||
|
|
|
@ -197,6 +197,8 @@ export default class MockText implements MockGameObject {
|
||||||
this.color = color;
|
this.color = color;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setInteractive = vi.fn();
|
||||||
|
|
||||||
setShadowColor(color) {
|
setShadowColor(color) {
|
||||||
// Sets the shadow color.
|
// Sets the shadow color.
|
||||||
// return this.phaserText.setShadowColor(color);
|
// return this.phaserText.setShadowColor(color);
|
||||||
|
|
|
@ -266,6 +266,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||||
private pokemonPassiveDisabledIcon: Phaser.GameObjects.Sprite;
|
private pokemonPassiveDisabledIcon: Phaser.GameObjects.Sprite;
|
||||||
private pokemonPassiveLockedIcon: Phaser.GameObjects.Sprite;
|
private pokemonPassiveLockedIcon: Phaser.GameObjects.Sprite;
|
||||||
|
|
||||||
|
private activeTooltip: "ABILITY" | "PASSIVE" | "CANDY" | undefined;
|
||||||
private instructionsContainer: Phaser.GameObjects.Container;
|
private instructionsContainer: Phaser.GameObjects.Container;
|
||||||
private filterInstructionsContainer: Phaser.GameObjects.Container;
|
private filterInstructionsContainer: Phaser.GameObjects.Container;
|
||||||
private shinyIconElement: Phaser.GameObjects.Sprite;
|
private shinyIconElement: Phaser.GameObjects.Sprite;
|
||||||
|
@ -561,10 +562,13 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||||
this.pokemonAbilityLabelText = addTextObject(this.scene, 6, 127 + starterInfoYOffset, i18next.t("starterSelectUiHandler:ability"), TextStyle.SUMMARY_ALT, { fontSize: starterInfoTextSize });
|
this.pokemonAbilityLabelText = addTextObject(this.scene, 6, 127 + starterInfoYOffset, i18next.t("starterSelectUiHandler:ability"), TextStyle.SUMMARY_ALT, { fontSize: starterInfoTextSize });
|
||||||
this.pokemonAbilityLabelText.setOrigin(0, 0);
|
this.pokemonAbilityLabelText.setOrigin(0, 0);
|
||||||
this.pokemonAbilityLabelText.setVisible(false);
|
this.pokemonAbilityLabelText.setVisible(false);
|
||||||
|
|
||||||
this.starterSelectContainer.add(this.pokemonAbilityLabelText);
|
this.starterSelectContainer.add(this.pokemonAbilityLabelText);
|
||||||
|
|
||||||
this.pokemonAbilityText = addTextObject(this.scene, starterInfoXPos, 127 + starterInfoYOffset, "", TextStyle.SUMMARY_ALT, { fontSize: starterInfoTextSize });
|
this.pokemonAbilityText = addTextObject(this.scene, starterInfoXPos, 127 + starterInfoYOffset, "", TextStyle.SUMMARY_ALT, { fontSize: starterInfoTextSize });
|
||||||
this.pokemonAbilityText.setOrigin(0, 0);
|
this.pokemonAbilityText.setOrigin(0, 0);
|
||||||
|
this.pokemonAbilityText.setInteractive(new Phaser.Geom.Rectangle(0, 0, 250, 55), Phaser.Geom.Rectangle.Contains);
|
||||||
|
|
||||||
this.starterSelectContainer.add(this.pokemonAbilityText);
|
this.starterSelectContainer.add(this.pokemonAbilityText);
|
||||||
|
|
||||||
this.pokemonPassiveLabelText = addTextObject(this.scene, 6, 136 + starterInfoYOffset, i18next.t("starterSelectUiHandler:passive"), TextStyle.SUMMARY_ALT, { fontSize: starterInfoTextSize });
|
this.pokemonPassiveLabelText = addTextObject(this.scene, 6, 136 + starterInfoYOffset, i18next.t("starterSelectUiHandler:passive"), TextStyle.SUMMARY_ALT, { fontSize: starterInfoTextSize });
|
||||||
|
@ -574,6 +578,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||||
|
|
||||||
this.pokemonPassiveText = addTextObject(this.scene, starterInfoXPos, 136 + starterInfoYOffset, "", TextStyle.SUMMARY_ALT, { fontSize: starterInfoTextSize });
|
this.pokemonPassiveText = addTextObject(this.scene, starterInfoXPos, 136 + starterInfoYOffset, "", TextStyle.SUMMARY_ALT, { fontSize: starterInfoTextSize });
|
||||||
this.pokemonPassiveText.setOrigin(0, 0);
|
this.pokemonPassiveText.setOrigin(0, 0);
|
||||||
|
this.pokemonPassiveText.setInteractive(new Phaser.Geom.Rectangle(0, 0, 250, 55), Phaser.Geom.Rectangle.Contains);
|
||||||
this.starterSelectContainer.add(this.pokemonPassiveText);
|
this.starterSelectContainer.add(this.pokemonPassiveText);
|
||||||
|
|
||||||
this.pokemonPassiveDisabledIcon = this.scene.add.sprite(starterInfoXPos, 137 + starterInfoYOffset, "icon_stop");
|
this.pokemonPassiveDisabledIcon = this.scene.add.sprite(starterInfoXPos, 137 + starterInfoYOffset, "icon_stop");
|
||||||
|
@ -1921,6 +1926,14 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||||
}
|
}
|
||||||
} while (newAbilityIndex !== this.abilityCursor);
|
} while (newAbilityIndex !== this.abilityCursor);
|
||||||
starterAttributes.ability = newAbilityIndex; // store the selected ability
|
starterAttributes.ability = newAbilityIndex; // store the selected ability
|
||||||
|
|
||||||
|
const { visible: tooltipVisible } = this.scene.ui.getTooltip();
|
||||||
|
|
||||||
|
if (tooltipVisible && this.activeTooltip === "ABILITY") {
|
||||||
|
const newAbility = allAbilities[this.lastSpecies.getAbility(newAbilityIndex)];
|
||||||
|
this.scene.ui.editTooltip(`${newAbility.name}`, `${newAbility.description}`);
|
||||||
|
}
|
||||||
|
|
||||||
this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, undefined, newAbilityIndex, undefined);
|
this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, undefined, newAbilityIndex, undefined);
|
||||||
success = true;
|
success = true;
|
||||||
}
|
}
|
||||||
|
@ -2687,12 +2700,30 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFriendship(speciesId: number) {
|
||||||
|
let currentFriendship = this.scene.gameData.starterData[speciesId].friendship;
|
||||||
|
if (!currentFriendship || currentFriendship === undefined) {
|
||||||
|
currentFriendship = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const friendshipCap = getStarterValueFriendshipCap(speciesStarters[speciesId]);
|
||||||
|
|
||||||
|
return { currentFriendship, friendshipCap };
|
||||||
|
}
|
||||||
|
|
||||||
setSpecies(species: PokemonSpecies | null) {
|
setSpecies(species: PokemonSpecies | null) {
|
||||||
this.speciesStarterDexEntry = species ? this.scene.gameData.dexData[species.speciesId] : null;
|
this.speciesStarterDexEntry = species ? this.scene.gameData.dexData[species.speciesId] : null;
|
||||||
this.dexAttrCursor = species ? this.getCurrentDexProps(species.speciesId) : 0n;
|
this.dexAttrCursor = species ? this.getCurrentDexProps(species.speciesId) : 0n;
|
||||||
this.abilityCursor = species ? this.scene.gameData.getStarterSpeciesDefaultAbilityIndex(species) : 0;
|
this.abilityCursor = species ? this.scene.gameData.getStarterSpeciesDefaultAbilityIndex(species) : 0;
|
||||||
this.natureCursor = species ? this.scene.gameData.getSpeciesDefaultNature(species) : 0;
|
this.natureCursor = species ? this.scene.gameData.getSpeciesDefaultNature(species) : 0;
|
||||||
|
|
||||||
|
if (!species && this.scene.ui.getTooltip().visible) {
|
||||||
|
this.scene.ui.hideTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pokemonAbilityText.off("pointerover");
|
||||||
|
this.pokemonPassiveText.off("pointerover");
|
||||||
|
|
||||||
const starterAttributes : StarterAttributes | null = species ? {...this.starterPreferences[species.speciesId]} : null;
|
const starterAttributes : StarterAttributes | null = species ? {...this.starterPreferences[species.speciesId]} : null;
|
||||||
|
|
||||||
if (starterAttributes?.nature) {
|
if (starterAttributes?.nature) {
|
||||||
|
@ -2807,17 +2838,18 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||||
this.pokemonHatchedIcon.setVisible(true);
|
this.pokemonHatchedIcon.setVisible(true);
|
||||||
this.pokemonHatchedCountText.setVisible(true);
|
this.pokemonHatchedCountText.setVisible(true);
|
||||||
|
|
||||||
let currentFriendship = this.scene.gameData.starterData[this.lastSpecies.speciesId].friendship;
|
const { currentFriendship, friendshipCap } = this.getFriendship(this.lastSpecies.speciesId);
|
||||||
if (!currentFriendship || currentFriendship === undefined) {
|
|
||||||
currentFriendship = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const friendshipCap = getStarterValueFriendshipCap(speciesStarters[this.lastSpecies.speciesId]);
|
|
||||||
const candyCropY = 16 - (16 * (currentFriendship / friendshipCap));
|
const candyCropY = 16 - (16 * (currentFriendship / friendshipCap));
|
||||||
|
|
||||||
if (this.pokemonCandyDarknessOverlay.visible) {
|
if (this.pokemonCandyDarknessOverlay.visible) {
|
||||||
this.pokemonCandyDarknessOverlay.on("pointerover", () => (this.scene as BattleScene).ui.showTooltip("", `${currentFriendship}/${friendshipCap}`, true));
|
this.pokemonCandyDarknessOverlay.on("pointerover", () => {
|
||||||
this.pokemonCandyDarknessOverlay.on("pointerout", () => (this.scene as BattleScene).ui.hideTooltip());
|
this.scene.ui.showTooltip("", `${currentFriendship}/${friendshipCap}`, true);
|
||||||
|
this.activeTooltip = "CANDY";
|
||||||
|
});
|
||||||
|
this.pokemonCandyDarknessOverlay.on("pointerout", () => {
|
||||||
|
this.scene.ui.hideTooltip();
|
||||||
|
this.activeTooltip = undefined;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pokemonCandyDarknessOverlay.setCrop(0, 0, 16, candyCropY);
|
this.pokemonCandyDarknessOverlay.setCrop(0, 0, 16, candyCropY);
|
||||||
|
@ -2932,6 +2964,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||||
this.abilityCursor = -1;
|
this.abilityCursor = -1;
|
||||||
this.natureCursor = -1;
|
this.natureCursor = -1;
|
||||||
|
|
||||||
|
if (this.activeTooltip === "CANDY") {
|
||||||
|
const { currentFriendship, friendshipCap } = this.getFriendship(this.lastSpecies.speciesId);
|
||||||
|
this.scene.ui.editTooltip("", `${currentFriendship}/${friendshipCap}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (species?.forms?.find(f => f.formKey === "female")) {
|
if (species?.forms?.find(f => f.formKey === "female")) {
|
||||||
if (female !== undefined) {
|
if (female !== undefined) {
|
||||||
formIndex = female ? 1 : 0;
|
formIndex = female ? 1 : 0;
|
||||||
|
@ -3081,8 +3118,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dexEntry.caughtAttr) {
|
if (dexEntry.caughtAttr) {
|
||||||
const ability = this.lastSpecies.getAbility(abilityIndex!); // TODO: is this bang correct?
|
const ability = allAbilities[this.lastSpecies.getAbility(abilityIndex!)]; // TODO: is this bang correct?
|
||||||
this.pokemonAbilityText.setText(allAbilities[ability].name);
|
this.pokemonAbilityText.setText(ability.name);
|
||||||
|
|
||||||
const isHidden = abilityIndex === (this.lastSpecies.ability2 ? 2 : 1);
|
const isHidden = abilityIndex === (this.lastSpecies.ability2 ? 2 : 1);
|
||||||
this.pokemonAbilityText.setColor(this.getTextColor(!isHidden ? TextStyle.SUMMARY_ALT : TextStyle.SUMMARY_GOLD));
|
this.pokemonAbilityText.setColor(this.getTextColor(!isHidden ? TextStyle.SUMMARY_ALT : TextStyle.SUMMARY_GOLD));
|
||||||
|
@ -3091,6 +3128,21 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||||
const passiveAttr = this.scene.gameData.starterData[species.speciesId].passiveAttr;
|
const passiveAttr = this.scene.gameData.starterData[species.speciesId].passiveAttr;
|
||||||
const passiveAbility = allAbilities[starterPassiveAbilities[this.lastSpecies.speciesId]];
|
const passiveAbility = allAbilities[starterPassiveAbilities[this.lastSpecies.speciesId]];
|
||||||
|
|
||||||
|
if (this.pokemonAbilityText.visible) {
|
||||||
|
if (this.activeTooltip === "ABILITY") {
|
||||||
|
this.scene.ui.editTooltip(`${ability.name}`, `${ability.description}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pokemonAbilityText.on("pointerover", () => {
|
||||||
|
this.scene.ui.showTooltip(`${ability.name}`, `${ability.description}`, true);
|
||||||
|
this.activeTooltip = "ABILITY";
|
||||||
|
});
|
||||||
|
this.pokemonAbilityText.on("pointerout", () => {
|
||||||
|
this.scene.ui.hideTooltip();
|
||||||
|
this.activeTooltip = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (passiveAbility) {
|
if (passiveAbility) {
|
||||||
const isUnlocked = !!(passiveAttr & PassiveAttr.UNLOCKED);
|
const isUnlocked = !!(passiveAttr & PassiveAttr.UNLOCKED);
|
||||||
const isEnabled = !!(passiveAttr & PassiveAttr.ENABLED);
|
const isEnabled = !!(passiveAttr & PassiveAttr.ENABLED);
|
||||||
|
@ -3107,6 +3159,21 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||||
this.pokemonPassiveText.setAlpha(textAlpha);
|
this.pokemonPassiveText.setAlpha(textAlpha);
|
||||||
this.pokemonPassiveText.setShadowColor(this.getTextColor(textStyle, true));
|
this.pokemonPassiveText.setShadowColor(this.getTextColor(textStyle, true));
|
||||||
|
|
||||||
|
if (this.activeTooltip === "PASSIVE") {
|
||||||
|
this.scene.ui.editTooltip(`${passiveAbility.name}`, `${passiveAbility.description}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pokemonPassiveText.visible) {
|
||||||
|
this.pokemonPassiveText.on("pointerover", () => {
|
||||||
|
this.scene.ui.showTooltip(`${passiveAbility.name}`, `${passiveAbility.description}`, true);
|
||||||
|
this.activeTooltip = "PASSIVE";
|
||||||
|
});
|
||||||
|
this.pokemonPassiveText.on("pointerout", () => {
|
||||||
|
this.scene.ui.hideTooltip();
|
||||||
|
this.activeTooltip = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const iconPosition = {
|
const iconPosition = {
|
||||||
x: this.pokemonPassiveText.x + this.pokemonPassiveText.displayWidth + 1,
|
x: this.pokemonPassiveText.x + this.pokemonPassiveText.displayWidth + 1,
|
||||||
y: this.pokemonPassiveText.y + this.pokemonPassiveText.displayHeight / 2
|
y: this.pokemonPassiveText.y + this.pokemonPassiveText.displayHeight / 2
|
||||||
|
|
30
src/ui/ui.ts
30
src/ui/ui.ts
|
@ -244,7 +244,7 @@ export default class UI extends Phaser.GameObjects.Container {
|
||||||
|
|
||||||
this.tooltipContent = addTextObject(this.scene, 6, 16, "", TextStyle.TOOLTIP_CONTENT);
|
this.tooltipContent = addTextObject(this.scene, 6, 16, "", TextStyle.TOOLTIP_CONTENT);
|
||||||
this.tooltipContent.setName("text-tooltip-content");
|
this.tooltipContent.setName("text-tooltip-content");
|
||||||
this.tooltipContent.setWordWrapWidth(696);
|
this.tooltipContent.setWordWrapWidth(850);
|
||||||
|
|
||||||
this.tooltipContainer.add(this.tooltipBg);
|
this.tooltipContainer.add(this.tooltipBg);
|
||||||
this.tooltipContainer.add(this.tooltipTitle);
|
this.tooltipContainer.add(this.tooltipTitle);
|
||||||
|
@ -368,14 +368,13 @@ export default class UI extends Phaser.GameObjects.Container {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTooltip(): { visible: boolean; title: string; content: string } {
|
||||||
|
return { visible: this.tooltipContainer.visible, title: this.tooltipTitle.text, content: this.tooltipContent.text };
|
||||||
|
}
|
||||||
|
|
||||||
showTooltip(title: string, content: string, overlap?: boolean): void {
|
showTooltip(title: string, content: string, overlap?: boolean): void {
|
||||||
this.tooltipContainer.setVisible(true);
|
this.tooltipContainer.setVisible(true);
|
||||||
this.tooltipTitle.setText(title || "");
|
this.editTooltip(title, content);
|
||||||
const wrappedContent = this.tooltipContent.runWordWrap(content);
|
|
||||||
this.tooltipContent.setText(wrappedContent);
|
|
||||||
this.tooltipContent.y = title ? 16 : 4;
|
|
||||||
this.tooltipBg.width = Math.min(Math.max(this.tooltipTitle.displayWidth, this.tooltipContent.displayWidth) + 12, 684);
|
|
||||||
this.tooltipBg.height = (title ? 31 : 19) + 10.5 * (wrappedContent.split("\n").length - 1);
|
|
||||||
if (overlap) {
|
if (overlap) {
|
||||||
(this.scene as BattleScene).uiContainer.moveAbove(this.tooltipContainer, this);
|
(this.scene as BattleScene).uiContainer.moveAbove(this.tooltipContainer, this);
|
||||||
} else {
|
} else {
|
||||||
|
@ -383,6 +382,15 @@ export default class UI extends Phaser.GameObjects.Container {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editTooltip(title: string, content: string): void {
|
||||||
|
this.tooltipTitle.setText(title || "");
|
||||||
|
const wrappedContent = this.tooltipContent.runWordWrap(content);
|
||||||
|
this.tooltipContent.setText(wrappedContent);
|
||||||
|
this.tooltipContent.y = title ? 16 : 4;
|
||||||
|
this.tooltipBg.width = Math.min(Math.max(this.tooltipTitle.displayWidth, this.tooltipContent.displayWidth) + 12, 838);
|
||||||
|
this.tooltipBg.height = (title ? 31 : 19) + 10.5 * (wrappedContent.split("\n").length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
hideTooltip(): void {
|
hideTooltip(): void {
|
||||||
this.tooltipContainer.setVisible(false);
|
this.tooltipContainer.setVisible(false);
|
||||||
this.tooltipTitle.clearTint();
|
this.tooltipTitle.clearTint();
|
||||||
|
@ -390,8 +398,12 @@ export default class UI extends Phaser.GameObjects.Container {
|
||||||
|
|
||||||
update(): void {
|
update(): void {
|
||||||
if (this.tooltipContainer.visible) {
|
if (this.tooltipContainer.visible) {
|
||||||
const reverse = this.scene.game.input.mousePointer && this.scene.game.input.mousePointer.x >= this.scene.game.canvas.width - this.tooltipBg.width * 6 - 12;
|
const xReverse = this.scene.game.input.mousePointer && this.scene.game.input.mousePointer.x >= this.scene.game.canvas.width - this.tooltipBg.width * 6 - 12;
|
||||||
this.tooltipContainer.setPosition(!reverse ? this.scene.game.input.mousePointer!.x / 6 + 2 : this.scene.game.input.mousePointer!.x / 6 - this.tooltipBg.width - 2, this.scene.game.input.mousePointer!.y / 6 + 2); // TODO: are these bangs correct?
|
const yReverse = this.scene.game.input.mousePointer && this.scene.game.input.mousePointer.y >= this.scene.game.canvas.height - this.tooltipBg.height * 6 - 12;
|
||||||
|
this.tooltipContainer.setPosition(
|
||||||
|
!xReverse ? this.scene.game.input.mousePointer!.x / 6 + 2 : this.scene.game.input.mousePointer!.x / 6 - this.tooltipBg.width - 2,
|
||||||
|
!yReverse ? this.scene.game.input.mousePointer!.y / 6 + 2 : this.scene.game.input.mousePointer!.y / 6 - this.tooltipBg.height - 2,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue