mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-07-16 21:32:18 +02:00
Combined into Battle Info like I originally wanted
This commit is contained in:
parent
fcab6ee191
commit
19ec24db85
@ -48,7 +48,6 @@ import { BerryType } from '../data/berry';
|
|||||||
import i18next from '../plugins/i18n';
|
import i18next from '../plugins/i18n';
|
||||||
import { speciesEggMoves } from '../data/egg-moves';
|
import { speciesEggMoves } from '../data/egg-moves';
|
||||||
import { ModifierTier } from '../modifier/modifier-tier';
|
import { ModifierTier } from '../modifier/modifier-tier';
|
||||||
import BattleFlyout, { EnemyBattleFlyout, PlayerBattleFlyout } from '#app/ui/battle-flyout.js';
|
|
||||||
|
|
||||||
export enum FieldPosition {
|
export enum FieldPosition {
|
||||||
CENTER,
|
CENTER,
|
||||||
@ -67,7 +66,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
public variant: Variant;
|
public variant: Variant;
|
||||||
public pokeball: PokeballType;
|
public pokeball: PokeballType;
|
||||||
protected battleInfo: BattleInfo;
|
protected battleInfo: BattleInfo;
|
||||||
protected battleFlyout: BattleFlyout;
|
|
||||||
public level: integer;
|
public level: integer;
|
||||||
public exp: integer;
|
public exp: integer;
|
||||||
public levelExp: integer;
|
public levelExp: integer;
|
||||||
@ -217,16 +215,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
this.initBattleInfo();
|
this.initBattleInfo();
|
||||||
|
|
||||||
this.scene.fieldUI.addAt(this.battleFlyout, 0);
|
|
||||||
this.scene.fieldUI.addAt(this.battleInfo, 0);
|
this.scene.fieldUI.addAt(this.battleInfo, 0);
|
||||||
this.scene.fieldUI.moveBelow<Phaser.GameObjects.GameObject>(this.battleFlyout, this.battleInfo);
|
|
||||||
this.scene.fieldUI.moveDown(this.battleFlyout);
|
|
||||||
this.scene.fieldUI.moveDown(this.battleFlyout);
|
|
||||||
this.scene.fieldUI.moveDown(this.battleFlyout);
|
|
||||||
this.scene.fieldUI.moveDown(this.battleFlyout);
|
|
||||||
this.scene.fieldUI.moveDown(this.battleFlyout);
|
|
||||||
this.scene.fieldUI.moveDown(this.battleFlyout);
|
|
||||||
|
|
||||||
|
|
||||||
const getSprite = (hasShadow?: boolean) => {
|
const getSprite = (hasShadow?: boolean) => {
|
||||||
const ret = this.scene.addPokemonSprite(this, 0, 0, `pkmn__${this.isPlayer() ? 'back__' : ''}sub`, undefined, true);
|
const ret = this.scene.addPokemonSprite(this, 0, 0, `pkmn__${this.isPlayer() ? 'back__' : ''}sub`, undefined, true);
|
||||||
@ -534,9 +523,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
this.battleInfo.setMini(fieldPosition !== FieldPosition.CENTER);
|
this.battleInfo.setMini(fieldPosition !== FieldPosition.CENTER);
|
||||||
this.battleInfo.setOffset(fieldPosition === FieldPosition.RIGHT);
|
this.battleInfo.setOffset(fieldPosition === FieldPosition.RIGHT);
|
||||||
|
|
||||||
this.battleFlyout.setMini(fieldPosition !== FieldPosition.CENTER);
|
|
||||||
this.battleFlyout.setOffset(fieldPosition === FieldPosition.RIGHT);
|
|
||||||
|
|
||||||
const newOffset = this.getFieldPositionOffset();
|
const newOffset = this.getFieldPositionOffset();
|
||||||
|
|
||||||
let relX = newOffset[0] - initialOffset[0];
|
let relX = newOffset[0] - initialOffset[0];
|
||||||
@ -1363,51 +1349,25 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
ease: 'Cubic.easeOut'
|
ease: 'Cubic.easeOut'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!this.battleFlyout.visible) {
|
|
||||||
this.battleFlyout.setX(this.battleFlyout.x + (this.isPlayer() ? 150 : !this.isBoss() ? -150 : -198));
|
|
||||||
this.battleFlyout.setVisible(true);
|
|
||||||
this.scene.tweens.add({
|
|
||||||
targets: this.battleFlyout,
|
|
||||||
x: this.isPlayer() ? '-=150' : `+=${!this.isBoss() ? 150 : 246}`,
|
|
||||||
duration: 1000,
|
|
||||||
ease: 'Cubic.easeOut'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hideInfo(): Promise<void> {
|
hideInfo(): Promise<void> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
if (this.battleInfo.visible || this.battleFlyout.visible) {
|
if (this.battleInfo.visible) {
|
||||||
if (this.battleInfo.visible) {
|
this.scene.tweens.add({
|
||||||
this.scene.tweens.add({
|
targets: [ this.battleInfo, this.battleInfo.expMaskRect ],
|
||||||
targets: [ this.battleInfo, this.battleInfo.expMaskRect ],
|
x: this.isPlayer() ? '+=150' : `-=${!this.isBoss() ? 150 : 246}`,
|
||||||
x: this.isPlayer() ? '+=150' : `-=${!this.isBoss() ? 150 : 246}`,
|
duration: 500,
|
||||||
duration: 500,
|
ease: 'Cubic.easeIn',
|
||||||
ease: 'Cubic.easeIn',
|
onComplete: () => {
|
||||||
onComplete: () => {
|
if (this.isPlayer())
|
||||||
if (this.isPlayer())
|
this.battleInfo.expMaskRect.x -= 150;
|
||||||
this.battleInfo.expMaskRect.x -= 150;
|
this.battleInfo.setVisible(false);
|
||||||
this.battleInfo.setVisible(false);
|
this.battleInfo.setX(this.battleInfo.x - (this.isPlayer() ? 150 : !this.isBoss() ? -150 : -198));
|
||||||
this.battleInfo.setX(this.battleInfo.x - (this.isPlayer() ? 150 : !this.isBoss() ? -150 : -198));
|
resolve();
|
||||||
resolve();
|
}
|
||||||
}
|
});
|
||||||
});
|
} else
|
||||||
}
|
|
||||||
/* if(this.battleFlyout.visible) {
|
|
||||||
this.scene.tweens.add({
|
|
||||||
targets: this.battleFlyout,
|
|
||||||
x: this.isPlayer() ? '+=150' : `-=${!this.isBoss() ? 150 : 246}`,
|
|
||||||
duration: 500,
|
|
||||||
ease: 'Cubic.easeIn',
|
|
||||||
onComplete: () => {
|
|
||||||
this.battleFlyout.setVisible(false);
|
|
||||||
this.battleFlyout.setX(this.battleFlyout.x - (this.isPlayer() ? 150 : !this.isBoss() ? -150 : -198));
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} */
|
|
||||||
}
|
|
||||||
else
|
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1418,7 +1378,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
toggleStats(visible: boolean): void {
|
toggleStats(visible: boolean): void {
|
||||||
this.battleInfo.toggleStats(visible);
|
this.battleInfo.toggleStats(visible);
|
||||||
this.battleFlyout.toggleFlyout(visible);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addExp(exp: integer) {
|
addExp(exp: integer) {
|
||||||
@ -2554,7 +2513,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
this.battleInfo?.destroy();
|
this.battleInfo?.destroy();
|
||||||
this.battleFlyout?.destroy();
|
|
||||||
super.destroy();
|
super.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2584,9 +2542,6 @@ export class PlayerPokemon extends Pokemon {
|
|||||||
initBattleInfo(): void {
|
initBattleInfo(): void {
|
||||||
this.battleInfo = new PlayerBattleInfo(this.scene);
|
this.battleInfo = new PlayerBattleInfo(this.scene);
|
||||||
this.battleInfo.initInfo(this);
|
this.battleInfo.initInfo(this);
|
||||||
|
|
||||||
this.battleFlyout = new PlayerBattleFlyout(this.scene);
|
|
||||||
this.battleFlyout.initInfo(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isPlayer(): boolean {
|
isPlayer(): boolean {
|
||||||
@ -2960,9 +2915,6 @@ export class EnemyPokemon extends Pokemon {
|
|||||||
this.battleInfo = new EnemyBattleInfo(this.scene);
|
this.battleInfo = new EnemyBattleInfo(this.scene);
|
||||||
this.battleInfo.updateBossSegments(this);
|
this.battleInfo.updateBossSegments(this);
|
||||||
this.battleInfo.initInfo(this);
|
this.battleInfo.initInfo(this);
|
||||||
|
|
||||||
this.battleFlyout = new EnemyBattleFlyout(this.scene);
|
|
||||||
this.battleFlyout.initInfo(this);
|
|
||||||
} else
|
} else
|
||||||
this.battleInfo.updateBossSegments(this);
|
this.battleInfo.updateBossSegments(this);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import Pokemon from "#app/field/pokemon.js";
|
import { EnemyPokemon, default as Pokemon } from '../field/pokemon';
|
||||||
import { addTextObject, TextStyle } from "./text";
|
import { addTextObject, TextStyle } from "./text";
|
||||||
import * as Utils from '../utils';
|
import * as Utils from '../utils';
|
||||||
|
|
||||||
@ -8,7 +8,10 @@ export default class BattleFlyout extends Phaser.GameObjects.Container {
|
|||||||
private boss: boolean;
|
private boss: boolean;
|
||||||
private offset: boolean;
|
private offset: boolean;
|
||||||
|
|
||||||
private translationX = 100;
|
private flyoutWidth = 75;
|
||||||
|
private flyoutHeight = 22;
|
||||||
|
|
||||||
|
private translationX: number;
|
||||||
private anchorX: number;
|
private anchorX: number;
|
||||||
private anchorY: number;
|
private anchorY: number;
|
||||||
|
|
||||||
@ -18,28 +21,22 @@ export default class BattleFlyout extends Phaser.GameObjects.Container {
|
|||||||
private flyoutPlaceholder: Phaser.GameObjects.Rectangle;
|
private flyoutPlaceholder: Phaser.GameObjects.Rectangle;
|
||||||
private flyoutTextPlaceholder: Phaser.GameObjects.Text;
|
private flyoutTextPlaceholder: Phaser.GameObjects.Text;
|
||||||
|
|
||||||
constructor(scene: Phaser.Scene, x: number, y: number, player: boolean) {
|
constructor(scene: Phaser.Scene, player: boolean) {
|
||||||
super(scene, x, y);
|
super(scene, 0, 0);
|
||||||
this.player = player;
|
this.player = player;
|
||||||
this.mini = !player;
|
this.mini = !player;
|
||||||
this.boss = false;
|
this.boss = false;
|
||||||
this.offset = false;
|
this.offset = false;
|
||||||
|
|
||||||
// Initially invisible and shown via Pokemon.showInfo
|
|
||||||
this.setVisible(false);
|
|
||||||
this.setDepth(-15);
|
|
||||||
|
|
||||||
|
this.translationX = this.player ? -this.flyoutWidth : this.flyoutWidth;
|
||||||
this.anchorX = this.player ? -130 : 0;
|
this.anchorX = this.player ? -130 : 0;
|
||||||
this.anchorY = this.player ? -18.5 : -13;
|
this.anchorY = this.player ? -18.5 : -13;
|
||||||
|
|
||||||
this.flyoutParent = this.scene.add.container(this.anchorX - this.translationX, this.anchorY);
|
this.flyoutParent = this.scene.add.container(this.anchorX - this.translationX, this.anchorY);
|
||||||
this.flyoutParent.setAlpha(0);
|
this.flyoutParent.setAlpha(0);
|
||||||
this.flyoutParent.setDepth(-15);
|
|
||||||
|
|
||||||
this.add(this.flyoutParent);
|
this.add(this.flyoutParent);
|
||||||
|
|
||||||
this.flyoutContainer = this.scene.add.container(this.player ? -75 : 0, 0);
|
this.flyoutContainer = this.scene.add.container(this.player ? -this.flyoutWidth : 0, 0);
|
||||||
|
|
||||||
this.flyoutParent.add(this.flyoutContainer);
|
this.flyoutParent.add(this.flyoutContainer);
|
||||||
|
|
||||||
/* const color = this.player ? 0x00FF00 : 0xFF0000;
|
/* const color = this.player ? 0x00FF00 : 0xFF0000;
|
||||||
@ -49,7 +46,7 @@ export default class BattleFlyout extends Phaser.GameObjects.Container {
|
|||||||
this.flyoutContainer.add(this.scene.add.rectangle( 0, 22, 1, 1, color, 0.75).setOrigin(0.5, 0.5));
|
this.flyoutContainer.add(this.scene.add.rectangle( 0, 22, 1, 1, color, 0.75).setOrigin(0.5, 0.5));
|
||||||
this.flyoutContainer.add(this.scene.add.rectangle(maxX, 22, 1, 1, color + 200, 0.75).setOrigin(0.5, 0.5)); */
|
this.flyoutContainer.add(this.scene.add.rectangle(maxX, 22, 1, 1, color + 200, 0.75).setOrigin(0.5, 0.5)); */
|
||||||
|
|
||||||
this.flyoutPlaceholder = this.scene.add.rectangle(0, 0, 75, 22, 0xFFFFFF, 0.5);
|
this.flyoutPlaceholder = this.scene.add.rectangle(0, 0, this.flyoutWidth, this.flyoutHeight, 0xFFFFFF, 0.5);
|
||||||
this.flyoutPlaceholder.setOrigin(0, 0);
|
this.flyoutPlaceholder.setOrigin(0, 0);
|
||||||
|
|
||||||
this.flyoutContainer.add(this.flyoutPlaceholder);
|
this.flyoutContainer.add(this.flyoutPlaceholder);
|
||||||
@ -58,14 +55,6 @@ export default class BattleFlyout extends Phaser.GameObjects.Container {
|
|||||||
this.flyoutTextPlaceholder.setOrigin(0, 0);
|
this.flyoutTextPlaceholder.setOrigin(0, 0);
|
||||||
|
|
||||||
this.flyoutContainer.add(this.flyoutTextPlaceholder);
|
this.flyoutContainer.add(this.flyoutTextPlaceholder);
|
||||||
|
|
||||||
// Sets up the mask that hides the description text to give an illusion of scrolling
|
|
||||||
const flyoutMaskRect = this.scene.make.graphics({fillStyle: {color: 0xFFFFFF, alpha: 0.75}});
|
|
||||||
//flyoutMaskRect.setScale(6);
|
|
||||||
flyoutMaskRect.beginPath();
|
|
||||||
flyoutMaskRect.fillRect(this.flyoutParent.getWorldTransformMatrix().tx, this.flyoutParent.getWorldTransformMatrix().ty * -1, 100, 50);
|
|
||||||
|
|
||||||
//this.flyoutContainer.setMask(this.flyoutContainer.createGeometryMask(flyoutMaskRect));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initInfo(pokemon: Pokemon) {
|
initInfo(pokemon: Pokemon) {
|
||||||
@ -79,41 +68,17 @@ export default class BattleFlyout extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
this.mini = mini;
|
this.mini = mini;
|
||||||
|
|
||||||
if (this.player)
|
//if (this.player)
|
||||||
this.y -= 12 * (mini ? 1 : -1);
|
// this.y -= 12 * (mini ? 1 : -1);
|
||||||
}
|
|
||||||
|
|
||||||
setOffset(offset: boolean): void {
|
|
||||||
if (this.offset === offset)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.offset = offset;
|
|
||||||
|
|
||||||
this.x += 10 * (this.offset === this.player ? 1 : -1);
|
|
||||||
this.y += 27 * (this.offset ? 1 : -1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleFlyout(visible: boolean): void {
|
toggleFlyout(visible: boolean): void {
|
||||||
this.scene.tweens.add({
|
this.scene.tweens.add({
|
||||||
targets: this.flyoutParent,
|
targets: this.flyoutParent,
|
||||||
x: visible ? this.anchorX : this.anchorX + (this.player ? this.translationX : -this.translationX),
|
x: visible ? this.anchorX : this.anchorX - this.translationX,
|
||||||
duration: Utils.fixedInt(125),
|
duration: Utils.fixedInt(125),
|
||||||
ease: 'Sine.easeInOut',
|
ease: 'Sine.easeInOut',
|
||||||
alpha: visible ? 1 : 0,
|
alpha: visible ? 1 : 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export class PlayerBattleFlyout extends BattleFlyout {
|
|
||||||
constructor(scene: Phaser.Scene) {
|
|
||||||
super(scene, Math.floor(scene.game.canvas.width / 6) - 10, -72, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EnemyBattleFlyout extends BattleFlyout {
|
|
||||||
constructor(scene: Phaser.Scene) {
|
|
||||||
super(scene, 140, -141, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
setMini(mini: boolean): void { } // Always mini
|
|
||||||
}
|
}
|
@ -8,6 +8,7 @@ import BattleScene from '../battle-scene';
|
|||||||
import { Type, getTypeRgb } from '../data/type';
|
import { Type, getTypeRgb } from '../data/type';
|
||||||
import { getVariantTint } from '#app/data/variant';
|
import { getVariantTint } from '#app/data/variant';
|
||||||
import { BattleStat } from '#app/data/battle-stat';
|
import { BattleStat } from '#app/data/battle-stat';
|
||||||
|
import BattleFlyout from './battle-flyout';
|
||||||
|
|
||||||
const battleStatOrder = [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.ACC, BattleStat.EVA, BattleStat.SPD ];
|
const battleStatOrder = [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.ACC, BattleStat.EVA, BattleStat.SPD ];
|
||||||
|
|
||||||
@ -55,6 +56,8 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
|
|||||||
private statValuesContainer: Phaser.GameObjects.Container;
|
private statValuesContainer: Phaser.GameObjects.Container;
|
||||||
private statNumbers: Phaser.GameObjects.Sprite[];
|
private statNumbers: Phaser.GameObjects.Sprite[];
|
||||||
|
|
||||||
|
private flyoutMenu: BattleFlyout;
|
||||||
|
|
||||||
constructor(scene: Phaser.Scene, x: number, y: number, player: boolean) {
|
constructor(scene: Phaser.Scene, x: number, y: number, player: boolean) {
|
||||||
super(scene, x, y);
|
super(scene, x, y);
|
||||||
this.player = player;
|
this.player = player;
|
||||||
@ -197,6 +200,11 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
|
|||||||
this.statValuesContainer.add(statNumber);
|
this.statValuesContainer.add(statNumber);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.flyoutMenu = new BattleFlyout(this.scene, this.player);
|
||||||
|
this.add(this.flyoutMenu);
|
||||||
|
|
||||||
|
this.moveBelow<Phaser.GameObjects.GameObject>(this.flyoutMenu, this.box);
|
||||||
|
|
||||||
this.type1Icon = this.scene.add.sprite(player ? -139 : -15, player ? -17 : -15.5, `pbinfo_${player ? 'player' : 'enemy'}_type1`);
|
this.type1Icon = this.scene.add.sprite(player ? -139 : -15, player ? -17 : -15.5, `pbinfo_${player ? 'player' : 'enemy'}_type1`);
|
||||||
this.type1Icon.setOrigin(0, 0);
|
this.type1Icon.setOrigin(0, 0);
|
||||||
this.add(this.type1Icon);
|
this.add(this.type1Icon);
|
||||||
@ -214,9 +222,11 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
|
|||||||
this.updateNameText(pokemon);
|
this.updateNameText(pokemon);
|
||||||
const nameTextWidth = this.nameText.displayWidth;
|
const nameTextWidth = this.nameText.displayWidth;
|
||||||
|
|
||||||
this.name = pokemon.name
|
this.name = pokemon.name;
|
||||||
this.box.name = pokemon.name;
|
this.box.name = pokemon.name;
|
||||||
|
|
||||||
|
this.flyoutMenu.initInfo(pokemon);
|
||||||
|
|
||||||
this.genderText.setText(getGenderSymbol(pokemon.gender));
|
this.genderText.setText(getGenderSymbol(pokemon.gender));
|
||||||
this.genderText.setColor(getGenderColor(pokemon.gender));
|
this.genderText.setColor(getGenderColor(pokemon.gender));
|
||||||
this.genderText.setPositionRelative(this.nameText, nameTextWidth, 0);
|
this.genderText.setPositionRelative(this.nameText, nameTextWidth, 0);
|
||||||
@ -333,6 +343,8 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
this.mini = mini;
|
this.mini = mini;
|
||||||
|
|
||||||
|
this.flyoutMenu.setMini(this.mini);
|
||||||
|
|
||||||
this.box.setTexture(this.getTextureName());
|
this.box.setTexture(this.getTextureName());
|
||||||
this.statsBox.setTexture(`${this.getTextureName()}_stats`);
|
this.statsBox.setTexture(`${this.getTextureName()}_stats`);
|
||||||
|
|
||||||
@ -361,6 +373,8 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
|
|||||||
ease: 'Sine.easeInOut',
|
ease: 'Sine.easeInOut',
|
||||||
alpha: visible ? 1 : 0
|
alpha: visible ? 1 : 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.flyoutMenu.toggleFlyout(visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBossSegments(pokemon: EnemyPokemon): void {
|
updateBossSegments(pokemon: EnemyPokemon): void {
|
||||||
@ -407,8 +421,8 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
this.offset = offset;
|
this.offset = offset;
|
||||||
|
|
||||||
this.x += 10 * (offset === this.player ? 1 : -1);
|
this.x += 10 * (this.offset === this.player ? 1 : -1);
|
||||||
this.y += 27 * (offset ? 1 : -1);
|
this.y += 27 * (this.offset ? 1 : -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateInfo(pokemon: Pokemon, instant?: boolean): Promise<void> {
|
updateInfo(pokemon: Pokemon, instant?: boolean): Promise<void> {
|
||||||
|
Loading…
Reference in New Issue
Block a user