diff --git a/index.css b/index.css index 0ab0573bd72..8be2653415f 100644 --- a/index.css +++ b/index.css @@ -27,6 +27,10 @@ body { justify-content: center; } +#app > div:first-child { + transform-origin: top !important; +} + #touchControls:not(.visible) { display: none; } diff --git a/package-lock.json b/package-lock.json index c22159456fb..0c24c09737d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -618,9 +618,9 @@ "dev": true }, "node_modules/axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.3.tgz", + "integrity": "sha512-fWyNdeawGam70jXSVlKl+SUNVcL6j6W79CuSIPfi6HnDUmSCH6gyUys/HrqHeA/wU0Az41rRgean494d0Jb+ww==", "dev": true, "dependencies": { "follow-redirects": "^1.15.0", @@ -629,9 +629,9 @@ } }, "node_modules/axios-cache-interceptor": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/axios-cache-interceptor/-/axios-cache-interceptor-1.3.3.tgz", - "integrity": "sha512-i+AU7qIf3trytWR8KmQpGbNm47T9OllnM3j/jtbybG2CN0eSmPL7Szo0VzJZPtuoKXVINvRozieXDLCUREW5kw==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/axios-cache-interceptor/-/axios-cache-interceptor-1.4.1.tgz", + "integrity": "sha512-Ax4+PiGfNxpQvyF00t55nFzWoVnqW7slKCg9va6dbqiuAGIxRE8r1uMzunw8TKJ5iwLivFqAb0EeiLeUCxuZIw==", "dev": true, "dependencies": { "cache-parser": "1.2.4", diff --git a/public/images/ui/windows/window_1_thin.png b/public/images/ui/windows/window_1_thin.png new file mode 100644 index 00000000000..4648d812f17 Binary files /dev/null and b/public/images/ui/windows/window_1_thin.png differ diff --git a/public/images/ui/windows/window_1_xthin.png b/public/images/ui/windows/window_1_xthin.png new file mode 100644 index 00000000000..0ee6ffd00e1 Binary files /dev/null and b/public/images/ui/windows/window_1_xthin.png differ diff --git a/public/images/ui/windows/window_2_thin.png b/public/images/ui/windows/window_2_thin.png new file mode 100644 index 00000000000..281d538b997 Binary files /dev/null and b/public/images/ui/windows/window_2_thin.png differ diff --git a/public/images/ui/windows/window_2_xthin.png b/public/images/ui/windows/window_2_xthin.png new file mode 100644 index 00000000000..01e95113338 Binary files /dev/null and b/public/images/ui/windows/window_2_xthin.png differ diff --git a/public/images/ui/windows/window_3_thin.png b/public/images/ui/windows/window_3_thin.png new file mode 100644 index 00000000000..12cc6d60778 Binary files /dev/null and b/public/images/ui/windows/window_3_thin.png differ diff --git a/public/images/ui/windows/window_3_xthin.png b/public/images/ui/windows/window_3_xthin.png new file mode 100644 index 00000000000..57ac6427b79 Binary files /dev/null and b/public/images/ui/windows/window_3_xthin.png differ diff --git a/public/images/ui/windows/window_4_thin.png b/public/images/ui/windows/window_4_thin.png new file mode 100644 index 00000000000..b61e7d7b22c Binary files /dev/null and b/public/images/ui/windows/window_4_thin.png differ diff --git a/public/images/ui/windows/window_4_xthin.png b/public/images/ui/windows/window_4_xthin.png new file mode 100644 index 00000000000..45fec09adb0 Binary files /dev/null and b/public/images/ui/windows/window_4_xthin.png differ diff --git a/src/account.ts b/src/account.ts new file mode 100644 index 00000000000..0b831a02e5d --- /dev/null +++ b/src/account.ts @@ -0,0 +1,24 @@ +import { bypassLogin } from "./battle-scene"; +import * as Utils from "./utils"; + +export let loggedInUser = null; + +export function updateUserInfo(): Promise { + return new Promise(resolve => { + if (bypassLogin) { + loggedInUser = { username: 'Guest' }; + return resolve(true); + } + Utils.apiFetch('account/info').then(response => { + if (!response.ok) { + loggedInUser = null; + resolve(false); + return; + } + return response.json(); + }).then(jsonResponse => { + loggedInUser = jsonResponse; + resolve(true); + }); + }); +} \ No newline at end of file diff --git a/src/battle-phase.ts b/src/battle-phase.ts index aa3d1883281..b9f597babbd 100644 --- a/src/battle-phase.ts +++ b/src/battle-phase.ts @@ -8,7 +8,7 @@ export class BattlePhase { } start() { - console.log(`%cStart Phase ${this.constructor.name}`, 'color:green;') + console.log(`%cStart Phase ${this.constructor.name}`, 'color:green;'); } end() { diff --git a/src/battle-phases.ts b/src/battle-phases.ts index 13b4525d4a1..cea47a9f883 100644 --- a/src/battle-phases.ts +++ b/src/battle-phases.ts @@ -1,4 +1,4 @@ -import BattleScene, { startingLevel, startingWave } from "./battle-scene"; +import BattleScene, { bypassLogin, startingLevel, startingWave } from "./battle-scene"; import { default as Pokemon, PlayerPokemon, EnemyPokemon, PokemonMove, MoveResult, DamageResult, FieldPosition, HitResult, TurnMove } from "./pokemon"; import * as Utils from './utils'; import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveCategory, MoveEffectAttr, MoveFlags, Moves, MultiHitAttr, OverrideMoveEffectAttr, VariableAccuracyAttr, MoveTarget, OneHitKOAttr, getMoveTargets, MoveTargetSet, MoveEffectTrigger, CopyMoveAttr, AttackMove, SelfStatusMove, DelayedAttackAttr } from "./data/move"; @@ -36,6 +36,55 @@ import { TrainerType, trainerConfigs } from "./data/trainer-type"; import { EggHatchPhase } from "./egg-hatch-phase"; import { Egg } from "./data/egg"; import { vouchers } from "./system/voucher"; +import { updateUserInfo } from "./account"; + +export class LoginPhase extends BattlePhase { + private showText: boolean; + + constructor(scene: BattleScene, showText?: boolean) { + super(scene); + + this.showText = showText === undefined || !!showText; + } + + start(): void { + super.start(); + + this.scene.ui.setMode(Mode.LOADING, { buttonActions: [] }); + Utils.executeIf(bypassLogin || !!Utils.getCookie(Utils.sessionIdKey), updateUserInfo).then(success => { + if (!success) { + if (this.showText) + this.scene.ui.showText('Log in or create an account to start. No email required!'); + + this.scene.playSound('menu_open'); + + this.scene.ui.setMode(Mode.LOGIN_FORM, { + buttonActions: [ + () => { + this.scene.ui.playSelect(); + this.end(); + }, () => { + this.scene.playSound('menu_open'); + this.scene.ui.setMode(Mode.REGISTRATION_FORM, { + buttonActions: [ + () => { + this.scene.ui.playSelect(); + this.end(); + }, () => { + this.scene.unshiftPhase(new LoginPhase(this.scene, false)) + this.end(); + } + ] + }); + } + ] + }); + return null; + } else + this.end(); + }); + } +} export class CheckLoadPhase extends BattlePhase { private loaded: boolean; @@ -47,6 +96,8 @@ export class CheckLoadPhase extends BattlePhase { } start(): void { + super.start(); + if (!this.scene.gameData.hasSession()) return this.end(); @@ -312,10 +363,13 @@ export class EncounterPhase extends BattlePhase { this.scene.ui.setMode(Mode.MESSAGE).then(() => { if (!this.loaded) { - this.scene.gameData.saveSession(this.scene); - this.scene.gameData.saveSystem(); - } - this.doEncounter(); + this.scene.gameData.saveSystem().then(success => { + if (!success) + return this.scene.reset(true); + this.scene.gameData.saveSession(this.scene, true).then(() => this.doEncounter()); + }); + } else + this.doEncounter(); }); }); } @@ -2531,15 +2585,19 @@ export class UnlockPhase extends BattlePhase { start(): void { this.scene.time.delayedCall(2000, () => { this.scene.gameData.unlocks[this.unlockable] = true; - this.scene.gameData.saveSystem(); - this.scene.playSoundWithoutBgm('level_up_fanfare'); - this.scene.ui.setMode(Mode.MESSAGE); - this.scene.arenaBg.setVisible(false); - this.scene.ui.fadeIn(250).then(() => { - this.scene.ui.showText(`${getUnlockableName(this.unlockable)}\nhas been unlocked.`, null, () => { - this.scene.time.delayedCall(1500, () => this.scene.arenaBg.setVisible(true)); - this.end(); - }, null, true, 1500); + this.scene.gameData.saveSystem().then(success => { + if (success) { + this.scene.playSoundWithoutBgm('level_up_fanfare'); + this.scene.ui.setMode(Mode.MESSAGE); + this.scene.arenaBg.setVisible(false); + this.scene.ui.fadeIn(250).then(() => { + this.scene.ui.showText(`${getUnlockableName(this.unlockable)}\nhas been unlocked.`, null, () => { + this.scene.time.delayedCall(1500, () => this.scene.arenaBg.setVisible(true)); + this.end(); + }, null, true, 1500); + }); + } else + this.scene.reset(true); }); }); } diff --git a/src/battle-scene.ts b/src/battle-scene.ts index f9e9a75eb86..001c4cf9ac6 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1,7 +1,7 @@ import Phaser from 'phaser'; import { Biome } from './data/biome'; import UI, { Mode } from './ui/ui'; -import { EncounterPhase, SummonPhase, NextEncounterPhase, NewBiomeEncounterPhase, SelectBiomePhase, MessagePhase, CheckLoadPhase, TurnInitPhase, ReturnPhase, LevelCapPhase, TestMessagePhase, ShowTrainerPhase, TrainerMessageTestPhase } from './battle-phases'; +import { EncounterPhase, SummonPhase, NextEncounterPhase, NewBiomeEncounterPhase, SelectBiomePhase, MessagePhase, CheckLoadPhase, TurnInitPhase, ReturnPhase, LevelCapPhase, TestMessagePhase, ShowTrainerPhase, TrainerMessageTestPhase, LoginPhase } from './battle-phases'; import Pokemon, { PlayerPokemon, EnemyPokemon } from './pokemon'; import PokemonSpecies, { PokemonSpeciesFilter, allSpecies, getPokemonSpecies, initSpecies } from './data/pokemon-species'; import * as Utils from './utils'; @@ -39,9 +39,12 @@ import { Achv, ModifierAchv, achvs } from './system/achv'; import { GachaType } from './data/egg'; import { Voucher, vouchers } from './system/voucher'; import { Gender } from './data/gender'; +import UIPlugin from 'phaser3-rex-plugins/templates/ui/ui-plugin'; +import { WindowVariant, getWindowVariantSuffix } from './ui/window'; const enableAuto = true; const quickStart = false; +export const bypassLogin = false; export const startingLevel = 5; export const startingWave = 1; export const startingBiome = Biome.TOWN; @@ -52,6 +55,7 @@ export enum Button { DOWN, LEFT, RIGHT, + SUBMIT, ACTION, CANCEL, MENU, @@ -72,6 +76,8 @@ export interface PokeballCounts { export type AnySound = Phaser.Sound.WebAudioSound | Phaser.Sound.HTML5AudioSound | Phaser.Sound.NoAudioSound; export default class BattleScene extends Phaser.Scene { + public rexUI: UIPlugin; + public auto: boolean; public masterVolume: number = 0.5; public bgmVolume: number = 1; @@ -192,8 +198,10 @@ export default class BattleScene extends Phaser.Scene { this.loadImage('command_fight_labels', 'ui'); this.loadAtlas('prompt', 'ui'); this.loadImage('cursor', 'ui'); - for (let w = 1; w <= 4; w++) - this.loadImage(`window_${w}`, 'ui/windows'); + for (let wv of Utils.getEnumValues(WindowVariant)) { + for (let w = 1; w <= 4; w++) + this.loadImage(`window_${w}${getWindowVariantSuffix(wv)}`, 'ui/windows'); + } this.loadImage('namebox', 'ui'); this.loadImage('pbinfo_player', 'ui'); this.loadImage('pbinfo_player_mini', 'ui'); @@ -376,7 +384,7 @@ export default class BattleScene extends Phaser.Scene { populateAnims(); - //this.load.plugin('rexfilechooserplugin', 'https://raw.githubusercontent.com/rexrainbow/phaser3-rex-notes/master/dist/rexfilechooserplugin.min.js', true); + this.load.plugin('rextexteditplugin', 'https://raw.githubusercontent.com/rexrainbow/phaser3-rex-notes/master/dist/rextexteditplugin.min.js', true); } create() { @@ -512,6 +520,8 @@ export default class BattleScene extends Phaser.Scene { this.reset(); if (this.quickStart) { + this.newBattle(); + for (let s = 0; s < 3; s++) { const playerSpecies = this.randomSpecies(startingWave, startingLevel); const playerPokemon = new PlayerPokemon(this, playerSpecies, startingLevel, 0, 0); @@ -537,9 +547,10 @@ export default class BattleScene extends Phaser.Scene { if (enableAuto) initAutoPlay.apply(this); - if (!this.quickStart) + if (!this.quickStart) { + this.pushPhase(new LoginPhase(this)); this.pushPhase(new CheckLoadPhase(this)); - else + } else this.pushPhase(new EncounterPhase(this)); this.shiftPhase(); @@ -553,6 +564,7 @@ export default class BattleScene extends Phaser.Scene { [Button.DOWN]: [keyCodes.DOWN, keyCodes.S], [Button.LEFT]: [keyCodes.LEFT, keyCodes.A], [Button.RIGHT]: [keyCodes.RIGHT, keyCodes.D], + [Button.SUBMIT]: [keyCodes.ENTER], [Button.ACTION]: [keyCodes.ENTER, keyCodes.SPACE, keyCodes.Z], [Button.CANCEL]: [keyCodes.BACKSPACE, keyCodes.X], [Button.MENU]: [keyCodes.ESC, keyCodes.M], @@ -571,7 +583,7 @@ export default class BattleScene extends Phaser.Scene { const keys: Phaser.Input.Keyboard.Key[] = []; if (keyConfig.hasOwnProperty(b)) { for (let k of keyConfig[b]) - keys.push(this.input.keyboard.addKey(k)); + keys.push(this.input.keyboard.addKey(k, false)); mobileKeyConfig[Button[b]] = keys[0]; } this.buttonKeys[b] = keys; @@ -620,7 +632,7 @@ export default class BattleScene extends Phaser.Scene { return findInParty(this.getParty()) || findInParty(this.getEnemyParty()); } - reset(): void { + reset(clearScene?: boolean): void { this.seed = Utils.randomString(16); console.log('Seed:', this.seed); @@ -658,6 +670,23 @@ export default class BattleScene extends Phaser.Scene { this.trainer.setTexture('trainer_m'); this.trainer.setPosition(406, 132); + + if (clearScene) { + this.fadeOutBgm(250, false); + this.tweens.add({ + targets: [ this.uiContainer ], + alpha: 0, + duration: 250, + ease: 'Sine.easeInOut', + onComplete: () => { + this.clearPhaseQueue(); + + this.children.removeAll(true); + this.game.domContainer.innerHTML = ''; + this.launchBattle(); + } + }); + } } newBattle(waveIndex?: integer, battleType?: BattleType, trainerData?: TrainerData, double?: boolean): Battle { @@ -960,6 +989,8 @@ export default class BattleScene extends Phaser.Scene { } else if (this.isButtonPressed(Button.RIGHT)) { inputSuccess = this.ui.processInput(Button.RIGHT); vibrationLength = 5; + } else if (this.isButtonPressed(Button.SUBMIT)) { + inputSuccess = this.ui.processInput(Button.SUBMIT) || this.ui.processInput(Button.ACTION); } else if (this.isButtonPressed(Button.ACTION)) inputSuccess = this.ui.processInput(Button.ACTION); else if (this.isButtonPressed(Button.CANCEL)) { @@ -1117,7 +1148,7 @@ export default class BattleScene extends Phaser.Scene { fadeOutBgm(duration?: integer, destroy?: boolean): boolean { if (!this.bgm) - return; + return false; if (!duration) duration = 500; if (destroy === undefined) diff --git a/src/main.ts b/src/main.ts index 8af3a44c4b2..3c82a180c5c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,7 +2,9 @@ import Phaser from 'phaser'; import BattleScene from './battle-scene'; import InvertPostFX from './pipelines/invert'; import { version } from '../package.json'; +import UIPlugin from 'phaser3-rex-plugins/templates/ui/ui-plugin'; import BBCodeTextPlugin from 'phaser3-rex-plugins/plugins/bbcodetext-plugin'; +import InputTextPlugin from 'phaser3-rex-plugins/plugins/inputtext-plugin.js'; const config: Phaser.Types.Core.GameConfig = { type: Phaser.WEBGL, @@ -14,11 +16,31 @@ const config: Phaser.Types.Core.GameConfig = { }, plugins: { global: [{ + key: 'rexInputTextPlugin', + plugin: InputTextPlugin, + start: true + }, { key: 'rexBBCodeTextPlugin', plugin: BBCodeTextPlugin, start: true + }], + scene: [{ + key: 'rexUI', + plugin: UIPlugin, + mapping: 'rexUI' }] }, + input: { + mouse: { + target: 'app' + }, + touch: { + target: 'app' + }, + }, + dom: { + createContainer: true + }, pixelArt: true, pipeline: [ InvertPostFX ] as unknown as Phaser.Types.Core.PipelineConfig, scene: [ BattleScene ], diff --git a/src/system/game-data.ts b/src/system/game-data.ts index b3943725d28..e25703f7321 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -19,6 +19,7 @@ import { Egg } from "../data/egg"; import { VoucherType, vouchers } from "./voucher"; import { AES, enc } from "crypto-js"; import { Mode } from "../ui/ui"; +import { updateUserInfo } from "../account"; const saveKey = 'x0i2O7WRiANTqPmZ'; // Temporary; secure encryption is not yet necessary @@ -155,29 +156,35 @@ export class GameData { this.loadSystem(); } - public saveSystem(): boolean { - if (this.scene.quickStart) - return false; - - const data: SystemSaveData = { - trainerId: this.trainerId, - secretId: this.secretId, - dexData: this.dexData, - unlocks: this.unlocks, - achvUnlocks: this.achvUnlocks, - voucherUnlocks: this.voucherUnlocks, - voucherCounts: this.voucherCounts, - eggs: this.eggs.map(e => new EggData(e)), - gameVersion: this.scene.game.config.gameVersion, - timestamp: new Date().getTime() - }; + public saveSystem(): Promise { + return new Promise(resolve => { + if (this.scene.quickStart) + return resolve(true); - localStorage.setItem('data_bak', localStorage.getItem('data')); - - const maxIntAttrValue = Math.pow(2, 31); - localStorage.setItem('data', btoa(JSON.stringify(data, (k: any, v: any) => typeof v === 'bigint' ? v <= maxIntAttrValue ? Number(v) : v.toString() : v))); - - return true; + updateUserInfo().then(success => { + if (!success) + return resolve(false); + const data: SystemSaveData = { + trainerId: this.trainerId, + secretId: this.secretId, + dexData: this.dexData, + unlocks: this.unlocks, + achvUnlocks: this.achvUnlocks, + voucherUnlocks: this.voucherUnlocks, + voucherCounts: this.voucherCounts, + eggs: this.eggs.map(e => new EggData(e)), + gameVersion: this.scene.game.config.gameVersion, + timestamp: new Date().getTime() + }; + + localStorage.setItem('data_bak', localStorage.getItem('data')); + + const maxIntAttrValue = Math.pow(2, 31); + localStorage.setItem('data', btoa(JSON.stringify(data, (k: any, v: any) => typeof v === 'bigint' ? v <= maxIntAttrValue ? Number(v) : v.toString() : v))); + + resolve(true); + }); + }); } private loadSystem(): boolean { @@ -279,29 +286,36 @@ export class GameData { setSetting(this.scene, setting as Setting, settings[setting]); } - saveSession(scene: BattleScene): boolean { - const sessionData = { - seed: scene.seed, - gameMode: scene.gameMode, - party: scene.getParty().map(p => new PokemonData(p)), - enemyParty: scene.getEnemyParty().map(p => new PokemonData(p)), - modifiers: scene.findModifiers(() => true).map(m => new PersistentModifierData(m, true)), - enemyModifiers: scene.findModifiers(() => true, false).map(m => new PersistentModifierData(m, false)), - arena: new ArenaData(scene.arena), - pokeballCounts: scene.pokeballCounts, - money: scene.money, - waveIndex: scene.currentBattle.waveIndex, - battleType: scene.currentBattle.battleType, - trainer: scene.currentBattle.battleType == BattleType.TRAINER ? new TrainerData(scene.currentBattle.trainer) : null, - gameVersion: scene.game.config.gameVersion, - timestamp: new Date().getTime() - } as SessionSaveData; + saveSession(scene: BattleScene, skipVerification?: boolean): Promise { + return new Promise(resolve => { + Utils.executeIf(!skipVerification, updateUserInfo).then(success => { + if (success !== null && !success) + return resolve(false); - localStorage.setItem('sessionData', btoa(JSON.stringify(sessionData))); + const sessionData = { + seed: scene.seed, + gameMode: scene.gameMode, + party: scene.getParty().map(p => new PokemonData(p)), + enemyParty: scene.getEnemyParty().map(p => new PokemonData(p)), + modifiers: scene.findModifiers(() => true).map(m => new PersistentModifierData(m, true)), + enemyModifiers: scene.findModifiers(() => true, false).map(m => new PersistentModifierData(m, false)), + arena: new ArenaData(scene.arena), + pokeballCounts: scene.pokeballCounts, + money: scene.money, + waveIndex: scene.currentBattle.waveIndex, + battleType: scene.currentBattle.battleType, + trainer: scene.currentBattle.battleType == BattleType.TRAINER ? new TrainerData(scene.currentBattle.trainer) : null, + gameVersion: scene.game.config.gameVersion, + timestamp: new Date().getTime() + } as SessionSaveData; - console.debug('Session data saved'); + localStorage.setItem('sessionData', btoa(JSON.stringify(sessionData))); - return true; + console.debug('Session data saved'); + + resolve(true); + }); + }); } hasSession() { diff --git a/src/ui/abstact-option-select-ui-handler.ts b/src/ui/abstact-option-select-ui-handler.ts index 3c57fb5a0f1..071dd16b0b9 100644 --- a/src/ui/abstact-option-select-ui-handler.ts +++ b/src/ui/abstact-option-select-ui-handler.ts @@ -54,7 +54,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { this.optionSelectText.setPositionRelative(this.optionSelectBg, 16, 9); } - show(args: any[]) { + show(args: any[]): boolean { const options = this.getOptions(); if (args.length >= options.length && args.slice(0, options.length).filter(a => a instanceof Function).length === options.length) { @@ -64,7 +64,11 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { this.optionSelectContainer.setVisible(true); this.setCursor(0); + + return true; } + + return false; } processInput(button: Button): boolean { diff --git a/src/ui/achvs-ui-handler.ts b/src/ui/achvs-ui-handler.ts index edf770e57da..913a92bb129 100644 --- a/src/ui/achvs-ui-handler.ts +++ b/src/ui/achvs-ui-handler.ts @@ -105,7 +105,7 @@ export default class AchvsUiHandler extends MessageUiHandler { this.achvsContainer.setVisible(false); } - show(args: any[]) { + show(args: any[]): boolean { super.show(args); const achvUnlocks = this.scene.gameData.achvUnlocks; @@ -129,6 +129,8 @@ export default class AchvsUiHandler extends MessageUiHandler { this.getUi().moveTo(this.achvsContainer, this.getUi().length - 1); this.getUi().hideTooltip(); + + return true; } protected showAchv(achv: Achv) { diff --git a/src/ui/ball-ui-handler.ts b/src/ui/ball-ui-handler.ts index 0682f4ea317..a102b8bd2e0 100644 --- a/src/ui/ball-ui-handler.ts +++ b/src/ui/ball-ui-handler.ts @@ -48,12 +48,14 @@ export default class BallUiHandler extends UiHandler { this.setCursor(0); } - show(args: any[]) { + show(args: any[]): boolean { super.show(args); this.updateCounts(); this.pokeballSelectContainer.setVisible(true); this.setCursor(this.cursor); + + return true; } processInput(button: Button): boolean { diff --git a/src/ui/battle-message-ui-handler.ts b/src/ui/battle-message-ui-handler.ts index 88a0f323ddc..4cabd1723df 100644 --- a/src/ui/battle-message-ui-handler.ts +++ b/src/ui/battle-message-ui-handler.ts @@ -120,12 +120,14 @@ export default class BattleMessageUiHandler extends MessageUiHandler { this.levelUpStatsValuesContent = levelUpStatsValuesContent; } - show(args: any[]): void { + show(args: any[]): boolean { super.show(args); this.commandWindow.setVisible(false); this.movesWindowContainer.setVisible(false); this.message.setWordWrapWidth(1780); + + return true; } processInput(button: Button): boolean { diff --git a/src/ui/biome-select-ui-handler.ts b/src/ui/biome-select-ui-handler.ts index e7ba600c0ec..33c4e8217bb 100644 --- a/src/ui/biome-select-ui-handler.ts +++ b/src/ui/biome-select-ui-handler.ts @@ -36,7 +36,7 @@ export default class BiomeSelectUiHandler extends UiHandler { this.biomeSelectContainer.add(this.biomesText); } - show(args: any[]) { + show(args: any[]): boolean { if (args.length >= 2 && typeof(args[0]) === 'number' && args[1] instanceof Function) { super.show(args); @@ -59,6 +59,8 @@ export default class BiomeSelectUiHandler extends UiHandler { this.biomeSelectContainer.setVisible(true); this.setCursor(0); } + + return true; } processInput(button: Button): boolean { diff --git a/src/ui/command-ui-handler.ts b/src/ui/command-ui-handler.ts index 90f7a0172ad..ce8d7369941 100644 --- a/src/ui/command-ui-handler.ts +++ b/src/ui/command-ui-handler.ts @@ -37,7 +37,7 @@ export default class CommandUiHandler extends UiHandler { } } - show(args: any[]) { + show(args: any[]): boolean { super.show(args); this.fieldIndex = args.length ? args[0] as integer : 0; @@ -50,6 +50,8 @@ export default class CommandUiHandler extends UiHandler { messageHandler.message.setWordWrapWidth(1110); messageHandler.showText(`What will\n${(this.scene.getCurrentPhase() as CommandPhase).getPokemon().name} do?`, 0); this.setCursor(this.getCursor()); + + return true; } processInput(button: Button): boolean { diff --git a/src/ui/confirm-ui-handler.ts b/src/ui/confirm-ui-handler.ts index 8f2c24a280f..cde4fed7fa3 100644 --- a/src/ui/confirm-ui-handler.ts +++ b/src/ui/confirm-ui-handler.ts @@ -22,7 +22,7 @@ export default class ConfirmUiHandler extends AbstractOptionSelectUiHandler { return [ 'Yes', 'No' ]; } - show(args: any[]) { + show(args: any[]): boolean { if (args.length >= 2 && args[0] instanceof Function && args[1] instanceof Function) { super.show(args); @@ -33,7 +33,11 @@ export default class ConfirmUiHandler extends AbstractOptionSelectUiHandler { this.optionSelectContainer.x = (this.scene.game.canvas.width / 6) - 1 + xOffset; this.setCursor(this.switchCheck ? this.switchCheckCursor : 0); + + return true; } + + return false; } setCursor(cursor: integer): boolean { diff --git a/src/ui/egg-gacha-ui-handler.ts b/src/ui/egg-gacha-ui-handler.ts index c04ab912421..b7f59f2ffb0 100644 --- a/src/ui/egg-gacha-ui-handler.ts +++ b/src/ui/egg-gacha-ui-handler.ts @@ -219,7 +219,7 @@ export default class EggGachaUiHandler extends MessageUiHandler { this.setCursor(0); } - show(args: any[]): void { + show(args: any[]): boolean { super.show(args); this.getUi().showText(defaultText, 0); @@ -232,6 +232,8 @@ export default class EggGachaUiHandler extends MessageUiHandler { this.updateVoucherCounts(); this.eggGachaContainer.setVisible(true); + + return true; } getDelayValue(delay: integer) { @@ -240,10 +242,90 @@ export default class EggGachaUiHandler extends MessageUiHandler { return Utils.fixedInt(delay); } - pull(pullCount?: integer, count?: integer, eggs?: Egg[]) { + pull(pullCount?: integer, count?: integer, eggs?: Egg[]): void { this.eggGachaOptionsContainer.setVisible(false); this.setTransitioning(true); + const doPull = () => { + if (this.transitionCancelled) + return this.showSummary(eggs); + + const egg = this.scene.add.sprite(127, 75, 'egg', `egg_${eggs[count].getKey()}`); + egg.setScale(0.5); + + this.gachaContainers[this.gachaCursor].add(egg); + this.gachaContainers[this.gachaCursor].moveTo(egg, 1); + + const doPullAnim = () => { + this.scene.playSound('gacha_running', { loop: true }); + this.scene.time.delayedCall(this.getDelayValue(count ? 500 : 1250), () => { + this.scene.playSound('gacha_dispense'); + this.scene.time.delayedCall(this.getDelayValue(750), () => { + this.scene.sound.stopByKey('gacha_running'); + this.scene.tweens.add({ + targets: egg, + duration: this.getDelayValue(350), + y: 95, + ease: 'Bounce.easeOut', + onComplete: () => { + this.scene.time.delayedCall(this.getDelayValue(125), () => { + this.scene.playSound('pb_catch'); + this.gachaHatches[this.gachaCursor].play('open'); + this.scene.tweens.add({ + targets: egg, + duration: this.getDelayValue(350), + scale: 0.75, + ease: 'Sine.easeIn' + }); + this.scene.tweens.add({ + targets: egg, + y: 110, + duration: this.getDelayValue(350), + ease: 'Back.easeOut', + onComplete: () => { + this.gachaHatches[this.gachaCursor].play('close'); + this.scene.tweens.add({ + targets: egg, + y: 200, + duration: this.getDelayValue(350), + ease: 'Cubic.easeIn', + onComplete: () => { + if (++count < pullCount) + this.pull(pullCount, count, eggs); + else + this.showSummary(eggs); + } + }); + } + }); + }); + } + }); + }); + }); + }; + + if (!count) { + this.scene.playSound('gacha_dial'); + this.scene.tweens.add({ + targets: this.gachaKnobs[this.gachaCursor], + duration: this.getDelayValue(350), + angle: 90, + ease: 'Cubic.easeInOut', + onComplete: () => { + this.scene.tweens.add({ + targets: this.gachaKnobs[this.gachaCursor], + duration: this.getDelayValue(350), + angle: 0, + ease: 'Sine.easeInOut' + }); + this.scene.time.delayedCall(this.getDelayValue(350), doPullAnim); + } + }); + } else + doPullAnim(); + }; + if (!pullCount) pullCount = 1; if (!count) @@ -270,87 +352,15 @@ export default class EggGachaUiHandler extends MessageUiHandler { this.scene.gameData.eggs.push(egg); } - this.scene.gameData.saveSystem(); - } - - if (this.transitionCancelled) { - return this.showSummary(eggs); - } - - const egg = this.scene.add.sprite(127, 75, 'egg', `egg_${eggs[count].getKey()}`); - egg.setScale(0.5); - - this.gachaContainers[this.gachaCursor].add(egg); - this.gachaContainers[this.gachaCursor].moveTo(egg, 1); - - const doPullAnim = () => { - this.scene.playSound('gacha_running', { loop: true }); - this.scene.time.delayedCall(this.getDelayValue(count ? 500 : 1250), () => { - this.scene.playSound('gacha_dispense'); - this.scene.time.delayedCall(this.getDelayValue(750), () => { - this.scene.sound.stopByKey('gacha_running'); - this.scene.tweens.add({ - targets: egg, - duration: this.getDelayValue(350), - y: 95, - ease: 'Bounce.easeOut', - onComplete: () => { - this.scene.time.delayedCall(this.getDelayValue(125), () => { - this.scene.playSound('pb_catch'); - this.gachaHatches[this.gachaCursor].play('open'); - this.scene.tweens.add({ - targets: egg, - duration: this.getDelayValue(350), - scale: 0.75, - ease: 'Sine.easeIn' - }); - this.scene.tweens.add({ - targets: egg, - y: 110, - duration: this.getDelayValue(350), - ease: 'Back.easeOut', - onComplete: () => { - this.gachaHatches[this.gachaCursor].play('close'); - this.scene.tweens.add({ - targets: egg, - y: 200, - duration: this.getDelayValue(350), - ease: 'Cubic.easeIn', - onComplete: () => { - if (++count < pullCount) - this.pull(pullCount, count, eggs); - else - this.showSummary(eggs); - } - }); - } - }); - }); - } - }); - }); - }); - }; - - if (!count) { - this.scene.playSound('gacha_dial'); - this.scene.tweens.add({ - targets: this.gachaKnobs[this.gachaCursor], - duration: this.getDelayValue(350), - angle: 90, - ease: 'Cubic.easeInOut', - onComplete: () => { - this.scene.tweens.add({ - targets: this.gachaKnobs[this.gachaCursor], - duration: this.getDelayValue(350), - angle: 0, - ease: 'Sine.easeInOut' - }); - this.scene.time.delayedCall(this.getDelayValue(350), doPullAnim); - } + this.scene.gameData.saveSystem().then(success => { + if (!success) + return this.scene.reset(true); + doPull(); }); - } else - doPullAnim(); + return; + } + + doPull(); } showSummary(eggs: Egg[]): void { diff --git a/src/ui/egg-hatch-scene-handler.ts b/src/ui/egg-hatch-scene-handler.ts index db282b3ddb4..e4801b9e1c4 100644 --- a/src/ui/egg-hatch-scene-handler.ts +++ b/src/ui/egg-hatch-scene-handler.ts @@ -21,10 +21,12 @@ export default class EggHatchSceneHandler extends UiHandler { }); } - show(_args: any[]): void { + show(_args: any[]): boolean { super.show(_args); this.getUi().showText(null, 0); + + return true; } processInput(button: Button): boolean { diff --git a/src/ui/egg-list-ui-handler.ts b/src/ui/egg-list-ui-handler.ts index f0c6fc2843e..e554f8b02cf 100644 --- a/src/ui/egg-list-ui-handler.ts +++ b/src/ui/egg-list-ui-handler.ts @@ -88,7 +88,7 @@ export default class EggListUiHandler extends MessageUiHandler { this.cursor = -1; } - show(args: any[]): void { + show(args: any[]): boolean { super.show(args); this.eggListContainer.setVisible(true); @@ -115,6 +115,8 @@ export default class EggListUiHandler extends MessageUiHandler { } this.setCursor(0); + + return true; } processInput(button: Button): boolean { diff --git a/src/ui/evolution-scene-handler.ts b/src/ui/evolution-scene-handler.ts index e139f4e9437..baebdf752c0 100644 --- a/src/ui/evolution-scene-handler.ts +++ b/src/ui/evolution-scene-handler.ts @@ -19,10 +19,12 @@ export default class EvolutionSceneHandler extends UiHandler { this.scene.fieldUI.add(this.evolutionContainer); } - show(_args: any[]): void { + show(_args: any[]): boolean { super.show(_args); this.scene.fieldUI.bringToTop(this.evolutionContainer); + + return true; } processInput(button: Button): boolean { diff --git a/src/ui/fight-ui-handler.ts b/src/ui/fight-ui-handler.ts index 90fc9ca34e4..356c87e0fde 100644 --- a/src/ui/fight-ui-handler.ts +++ b/src/ui/fight-ui-handler.ts @@ -36,7 +36,7 @@ export default class FightUiHandler extends UiHandler { ui.add(this.ppText); } - show(args: any[]) { + show(args: any[]): boolean { super.show(args); this.fieldIndex = args.length ? args[0] as integer : 0; @@ -46,6 +46,8 @@ export default class FightUiHandler extends UiHandler { messageHandler.movesWindowContainer.setVisible(true); this.setCursor(this.getCursor()); this.displayMoves(); + + return true; } processInput(button: Button): boolean { diff --git a/src/ui/form-modal-ui-handler.ts b/src/ui/form-modal-ui-handler.ts new file mode 100644 index 00000000000..d912c1e20b1 --- /dev/null +++ b/src/ui/form-modal-ui-handler.ts @@ -0,0 +1,127 @@ +import BattleScene, { Button } from "../battle-scene"; +import { ModalConfig, ModalUiHandler } from "./modal-ui-handler"; +import { Mode } from "./ui"; +import { TextStyle, addTextInputObject, addTextObject, getTextColor } from "./text"; +import { WindowVariant, addWindow } from "./window"; +import InputText from "phaser3-rex-plugins/plugins/inputtext"; + +export interface FormModalConfig extends ModalConfig { + errorMessage?: string; +} + +export abstract class FormModalUiHandler extends ModalUiHandler { + protected editing: boolean; + protected inputContainers: Phaser.GameObjects.Container[]; + protected inputs: InputText[]; + protected errorMessage: Phaser.GameObjects.Text; + protected submitAction: Function; + + constructor(scene: BattleScene, mode?: Mode) { + super(scene, mode); + + this.editing = false; + this.inputContainers = []; + this.inputs = []; + } + + abstract getFields(): string[]; + + getHeight(config?: ModalConfig): number { + return 20 * this.getFields().length + (this.getModalTitle() ? 26 : 0) + ((config as FormModalConfig)?.errorMessage ? 12 : 0) + 28; + } + + getReadableErrorMessage(error: string): string { + if (error?.indexOf('connection refused') > -1) + return 'Could not connect to the server'; + + return error; + } + + setup(): void { + super.setup(); + + const fields = this.getFields(); + + const hasTitle = !!this.getModalTitle(); + + fields.forEach((field, f) => { + const label = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * f, field, TextStyle.TOOLTIP_CONTENT); + + this.modalContainer.add(label); + + const inputContainer = this.scene.add.container(70, (hasTitle ? 28 : 2) + 20 * f); + + const inputBg = addWindow(this.scene, 0, 0, 80, 16, false, false, 0, 0, WindowVariant.XTHIN); + + const input = addTextInputObject(this.scene, 4, -2, 440, 116, TextStyle.TOOLTIP_CONTENT, { type: field.indexOf('Password') > -1 ? 'password' : 'text', maxLength: 16 }); + input.setOrigin(0, 0); + + inputContainer.add(inputBg); + inputContainer.add(input); + this.modalContainer.add(inputContainer); + + this.inputContainers.push(inputContainer); + this.inputs.push(input); + }); + + this.errorMessage = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * (fields.length - 1) + 16, '', TextStyle.TOOLTIP_CONTENT); + this.errorMessage.setColor(getTextColor(TextStyle.SUMMARY_RED)); + this.errorMessage.setShadowColor(getTextColor(TextStyle.SUMMARY_RED, true)); + this.errorMessage.setVisible(false); + this.modalContainer.add(this.errorMessage); + } + + show(args: any[]): boolean { + if (super.show(args)) { + this.inputContainers.map(ic => ic.setVisible(true)); + + const config = args[0] as FormModalConfig; + + this.submitAction = config.buttonActions.length + ? config.buttonActions[0] + : null; + + if (this.buttonBgs.length) { + this.buttonBgs[0].off('pointerdown'); + this.buttonBgs[0].on('pointerdown', () => { + if (this.submitAction) + this.submitAction(); + }); + } + + return true; + } + + return false; + } + + processInput(button: Button): boolean { + if (button === Button.SUBMIT && this.submitAction) { + this.submitAction(); + return true; + } + + return false; + } + + sanitizeInputs(): void { + for (let input of this.inputs) + input.text = input.text.trim(); + } + + updateContainer(config?: ModalConfig): void { + super.updateContainer(config); + + this.errorMessage.setText(this.getReadableErrorMessage((config as FormModalConfig)?.errorMessage || '')); + this.errorMessage.setVisible(!!this.errorMessage.text); + } + + clear(): void { + super.clear(); + this.modalContainer.setVisible(false); + + this.inputContainers.map(ic => ic.setVisible(false)); + + this.submitAction = null; + } +} \ No newline at end of file diff --git a/src/ui/game-mode-select-ui-handler.ts b/src/ui/game-mode-select-ui-handler.ts index f5a89d23254..07921da4041 100644 --- a/src/ui/game-mode-select-ui-handler.ts +++ b/src/ui/game-mode-select-ui-handler.ts @@ -29,7 +29,7 @@ export default class GameModeSelectUiHandler extends AbstractOptionSelectUiHandl return ret; } - show(args: any[]) { + show(args: any[]): boolean { if (args.length === 2 && args[0] instanceof Function && args[1] instanceof Function) { this.setupOptions(); @@ -39,7 +39,11 @@ export default class GameModeSelectUiHandler extends AbstractOptionSelectUiHandl this.optionSelectContainer.setVisible(true); this.setCursor(0); + + return true; } + + return false; } processInput(button: Button): boolean { diff --git a/src/ui/loading-modal-ui-handler.ts b/src/ui/loading-modal-ui-handler.ts new file mode 100644 index 00000000000..11c96fe4d47 --- /dev/null +++ b/src/ui/loading-modal-ui-handler.ts @@ -0,0 +1,39 @@ +import BattleScene from "../battle-scene"; +import { ModalUiHandler } from "./modal-ui-handler"; +import { addTextObject, TextStyle } from "./text"; +import { Mode } from "./ui"; + +export default class LoadingModalUiHandler extends ModalUiHandler { + constructor(scene: BattleScene, mode?: Mode) { + super(scene, mode); + } + + getModalTitle(): string { + return ''; + } + + getWidth(): number { + return 80; + } + + getHeight(): number { + return 32; + } + + getMargin(): [number, number, number, number] { + return [ 0, 0, 48, 0 ]; + } + + getButtonLabels(): string[] { + return [ ]; + } + + setup(): void { + super.setup(); + + const label = addTextObject(this.scene, this.getWidth() / 2, this.getHeight() / 2, 'Loading...', TextStyle.WINDOW); + label.setOrigin(0.5, 0.5); + + this.modalContainer.add(label); + } +} \ No newline at end of file diff --git a/src/ui/login-form-ui-handler.ts b/src/ui/login-form-ui-handler.ts new file mode 100644 index 00000000000..27c841b82a6 --- /dev/null +++ b/src/ui/login-form-ui-handler.ts @@ -0,0 +1,81 @@ +import { FormModalUiHandler } from "./form-modal-ui-handler"; +import { ModalConfig } from "./modal-ui-handler"; +import * as Utils from "../utils"; +import { Mode } from "./ui"; + +export default class LoginFormUiHandler extends FormModalUiHandler { + getModalTitle(config?: ModalConfig): string { + return 'Login'; + } + + getFields(config?: ModalConfig): string[] { + return [ 'Username', 'Password' ]; + } + + getWidth(config?: ModalConfig): number { + return 160; + } + + getMargin(config?: ModalConfig): [number, number, number, number] { + return [ 0, 0, 48, 0 ]; + } + + getButtonLabels(config?: ModalConfig): string[] { + return [ 'Log In', 'Register' ]; + } + + getReadableErrorMessage(error: string): string { + let colonIndex = error?.indexOf(':'); + if (colonIndex > 0) + error = error.slice(0, colonIndex); + switch (error) { + case 'invalid username': + return 'The provided username is invalid'; + case 'invalid password': + return 'The provided password is invalid'; + case 'account doesn\'t exist': + return 'The provided user does not exist'; + case 'password doesn\'t match': + return 'The provided password does not match'; + } + + return super.getReadableErrorMessage(error); + } + + show(args: any[]): boolean { + if (super.show(args)) { + const config = args[0] as ModalConfig; + + const originalLoginAction = this.submitAction; + this.submitAction = (_) => { + // Prevent overlapping overrides on action modification + this.submitAction = originalLoginAction; + this.sanitizeInputs(); + this.scene.ui.setMode(Mode.LOADING, { buttonActions: [] }); + const onFail = error => { + this.scene.ui.setMode(Mode.LOGIN_FORM, Object.assign(config, { errorMessage: error?.trim() })); + this.scene.ui.playError(); + }; + if (!this.inputs[0].text) + return onFail('Username must not be empty'); + Utils.apiPost('account/login', JSON.stringify({ username: this.inputs[0].text, password: this.inputs[1].text })) + .then(response => { + if (!response.ok) + return response.text(); + return response.json(); + }) + .then(response => { + if (response.hasOwnProperty('token')) { + Utils.setCookie(Utils.sessionIdKey, response.token); + originalLoginAction(); + } else + onFail(response); + }); + }; + + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index 94a22af0639..41d771fcf2e 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -1,14 +1,14 @@ import BattleScene, { Button } from "../battle-scene"; import { TextStyle, addTextObject } from "./text"; import { Mode } from "./ui"; -import UiHandler from "./ui-handler"; import * as Utils from "../utils"; import { addWindow } from "./window"; import MessageUiHandler from "./message-ui-handler"; import { GameDataType } from "../system/game-data"; +import { CheckLoadPhase, LoginPhase } from "../battle-phases"; export enum MenuOptions { - SETTINGS, + GAME_SETTINGS, ACHIEVEMENTS, VOUCHERS, EGG_LIST, @@ -16,7 +16,8 @@ export enum MenuOptions { IMPORT_SESSION, EXPORT_SESSION, IMPORT_DATA, - EXPORT_DATA + EXPORT_DATA, + LOG_OUT } export default class MenuUiHandler extends MessageUiHandler { @@ -73,7 +74,7 @@ export default class MenuUiHandler extends MessageUiHandler { this.menuContainer.setVisible(false); } - show(args: any[]) { + show(args: any[]): boolean { super.show(args); this.menuContainer.setVisible(true); @@ -84,6 +85,8 @@ export default class MenuUiHandler extends MessageUiHandler { this.getUi().hideTooltip(); this.scene.playSound('menu_open'); + + return true; } processInput(button: Button): boolean { @@ -94,7 +97,7 @@ export default class MenuUiHandler extends MessageUiHandler { if (button === Button.ACTION) { switch (this.cursor as MenuOptions) { - case MenuOptions.SETTINGS: + case MenuOptions.GAME_SETTINGS: this.scene.ui.setOverlayMode(Mode.SETTINGS); success = true; break; @@ -129,6 +132,26 @@ export default class MenuUiHandler extends MessageUiHandler { this.scene.gameData.exportData(this.cursor === MenuOptions.EXPORT_DATA ? GameDataType.SYSTEM : GameDataType.SESSION); success = true; break; + case MenuOptions.LOG_OUT: + success = true; + const doLogout = () => { + Utils.apiPost('account/logout').then(res => { + if (!res.ok) + console.error(`Log out failed (${res.status}: ${res.statusText})`); + Utils.setCookie(Utils.sessionIdKey, ''); + this.scene.reset(true); + }); + }; + if (this.scene.currentBattle) { + this.scene.ui.showText('You will lose any progress since the beginning of the battle. Proceed?', null, () => { + this.scene.ui.setOverlayMode(Mode.CONFIRM, doLogout, () => { + this.scene.ui.revertMode(); + this.scene.ui.showText(null, 0); + }, false, 98); + }); + } else + doLogout(); + break; } } else if (button === Button.CANCEL) { success = true; @@ -141,7 +164,7 @@ export default class MenuUiHandler extends MessageUiHandler { success = this.setCursor(this.cursor - 1); break; case Button.DOWN: - if (this.cursor < Utils.getEnumKeys(MenuOptions).length) + if (this.cursor + 1 < Utils.getEnumKeys(MenuOptions).length) success = this.setCursor(this.cursor + 1); break; } @@ -152,7 +175,7 @@ export default class MenuUiHandler extends MessageUiHandler { else if (error) ui.playError(); - return true; + return success || error; } showText(text: string, delay?: number, callback?: Function, callbackDelay?: number, prompt?: boolean, promptDelay?: number): void { diff --git a/src/ui/modal-ui-handler.ts b/src/ui/modal-ui-handler.ts new file mode 100644 index 00000000000..4b4d87f9574 --- /dev/null +++ b/src/ui/modal-ui-handler.ts @@ -0,0 +1,130 @@ +import BattleScene, { Button } from "../battle-scene"; +import { TextStyle, addTextObject } from "./text"; +import { Mode } from "./ui"; +import UiHandler from "./ui-handler"; +import { WindowVariant, addWindow } from "./window"; + +export interface ModalConfig { + buttonActions: Function[]; +} + +export abstract class ModalUiHandler extends UiHandler { + protected modalContainer: Phaser.GameObjects.Container; + protected modalBg: Phaser.GameObjects.NineSlice; + protected titleText: Phaser.GameObjects.Text; + protected buttonContainers: Phaser.GameObjects.Container[]; + protected buttonBgs: Phaser.GameObjects.NineSlice[]; + + constructor(scene: BattleScene, mode?: Mode) { + super(scene, mode); + + this.buttonContainers = []; + this.buttonBgs = []; + } + + abstract getModalTitle(config?: ModalConfig): string; + + abstract getWidth(config?: ModalConfig): number; + + abstract getHeight(config?: ModalConfig): number; + + abstract getMargin(config?: ModalConfig): [number, number, number, number]; + + abstract getButtonLabels(config?: ModalConfig): string[]; + + setup() { + const ui = this.getUi(); + + this.modalContainer = this.scene.add.container(0, 0); + + this.modalContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 6, this.scene.game.canvas.height / 6), Phaser.Geom.Rectangle.Contains); + + this.modalBg = addWindow(this.scene, 0, 0, 0, 0); + + this.modalContainer.add(this.modalBg); + + this.titleText = addTextObject(this.scene, 0, 4, '', TextStyle.SETTINGS_LABEL); + this.titleText.setOrigin(0.5, 0); + + this.modalContainer.add(this.titleText); + + ui.add(this.modalContainer); + + const buttonLabels = this.getButtonLabels(); + + for (let label of buttonLabels) { + const buttonLabel = addTextObject(this.scene, 0, 8, label, TextStyle.TOOLTIP_CONTENT); + buttonLabel.setOrigin(0.5, 0.5); + + const buttonBg = addWindow(this.scene, 0, 0, buttonLabel.getBounds().width + 8, 16, false, false, 0, 0, WindowVariant.THIN); + buttonBg.setOrigin(0.5, 0); + buttonBg.setInteractive(new Phaser.Geom.Rectangle(0, 0, buttonBg.width, buttonBg.height), Phaser.Geom.Rectangle.Contains); + + const buttonContainer = this.scene.add.container(0, 0); + + this.buttonBgs.push(buttonBg); + this.buttonContainers.push(buttonContainer); + + buttonContainer.add(buttonBg); + buttonContainer.add(buttonLabel); + this.modalContainer.add(buttonContainer); + } + + this.modalContainer.setVisible(false); + } + + show(args: any[]): boolean { + if (args.length >= 1 && 'buttonActions' in args[0]) { + super.show(args); + + const config = args[0] as ModalConfig; + + this.updateContainer(config); + + this.modalContainer.setVisible(true); + + this.getUi().moveTo(this.modalContainer, this.getUi().length - 1); + + for (let a = 0; a < this.buttonBgs.length; a++) { + if (a < this.buttonBgs.length) + this.buttonBgs[a].on('pointerdown', (_) => config.buttonActions[a]()); + } + + return true; + } + + return false; + } + + updateContainer(config?: ModalConfig): void { + const [ marginTop, marginRight, marginBottom, marginLeft ] = this.getMargin(config); + + const [ width, height ] = [ this.getWidth(config), this.getHeight(config) ]; + this.modalContainer.setPosition((((this.scene.game.canvas.width / 6) - (width + (marginRight - marginLeft))) / 2), (((-this.scene.game.canvas.height / 6) - (height + (marginBottom - marginTop))) / 2)); + + this.modalBg.setSize(width, height); + + const title = this.getModalTitle(config); + + this.titleText.setText(title); + this.titleText.setX(width / 2); + this.titleText.setVisible(!!title); + + for (let b = 0; b < this.buttonContainers.length; b++) { + const sliceWidth = width / (this.buttonContainers.length + 1); + + this.buttonContainers[b].setPosition(sliceWidth * (b + 1), this.modalBg.height - (this.buttonBgs[b].height + 8)); + } + } + + processInput(button: Button): boolean { + return false; + } + + clear() { + super.clear(); + this.modalContainer.setVisible(false); + + this.buttonBgs.map(bg => bg.off('pointerdown')); + } +} \ No newline at end of file diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index 8c50fbef96c..bb49eff61dd 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -53,17 +53,17 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.rerollButtonContainer.add(this.rerollCostText); } - show(args: any[]) { + show(args: any[]): boolean { if (this.active) { if (args.length >= 3) { this.awaitingActionInput = true; this.onActionInput = args[2]; } - return; + return false; } if (args.length !== 4 || !(args[1] instanceof Array) || !args[1].length || !(args[2] instanceof Function)) - return; + return false; super.show(args); @@ -135,6 +135,8 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.awaitingActionInput = true; this.onActionInput = args[2]; }); + + return true; } processInput(button: Button): boolean { diff --git a/src/ui/option-select-ui-handler.ts b/src/ui/option-select-ui-handler.ts index 6642233f42f..9d14ec5867f 100644 --- a/src/ui/option-select-ui-handler.ts +++ b/src/ui/option-select-ui-handler.ts @@ -21,9 +21,9 @@ export default class OptionSelectUiHandler extends AbstractOptionSelectUiHandler return this.options; } - show(args: any[]) { + show(args: any[]): boolean { if (args.length < 2 || args.length % 2 === 1) - return; + return false; const optionNames: string[] = []; const optionFuncs: Function[] = []; @@ -34,6 +34,6 @@ export default class OptionSelectUiHandler extends AbstractOptionSelectUiHandler this.setupOptions(); - super.show(optionFuncs); + return super.show(optionFuncs); } } \ No newline at end of file diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index 62ef6317298..e40f5a76bd7 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -161,9 +161,9 @@ export default class PartyUiHandler extends MessageUiHandler { this.partySlots = []; } - show(args: any[]) { + show(args: any[]): boolean { if (!args.length || this.active) - return; + return false; super.show(args); @@ -184,6 +184,8 @@ export default class PartyUiHandler extends MessageUiHandler { this.partyBg.setTexture(`party_bg${this.scene.currentBattle.double ? '_double' : ''}`); this.populatePartySlots(); this.setCursor(this.cursor < 6 ? this.cursor : 0); + + return true; } processInput(button: Button): boolean { diff --git a/src/ui/registration-form-ui-handler.ts b/src/ui/registration-form-ui-handler.ts new file mode 100644 index 00000000000..c1cc79f6939 --- /dev/null +++ b/src/ui/registration-form-ui-handler.ts @@ -0,0 +1,90 @@ +import { FormModalUiHandler } from "./form-modal-ui-handler"; +import { ModalConfig } from "./modal-ui-handler"; +import * as Utils from "../utils"; +import { Mode } from "./ui"; + +export default class RegistrationFormUiHandler extends FormModalUiHandler { + getModalTitle(config?: ModalConfig): string { + return 'Register'; + } + + getFields(config?: ModalConfig): string[] { + return [ 'Username', 'Password', 'Confirm Password' ]; + } + + getWidth(config?: ModalConfig): number { + return 160; + } + + getMargin(config?: ModalConfig): [number, number, number, number] { + return [ 0, 0, 48, 0 ]; + } + + getButtonLabels(config?: ModalConfig): string[] { + return [ 'Register', 'Back to Login' ]; + } + + getReadableErrorMessage(error: string): string { + let colonIndex = error?.indexOf(':'); + if (colonIndex > 0) + error = error.slice(0, colonIndex); + switch (error) { + case 'invalid username': + return 'Username must only contain letters, numbers, or underscores'; + case 'invalid password': + return 'Password must be 6 characters or longer'; + case 'failed to add account record': + return 'The provided username is already in use'; + } + + return super.getReadableErrorMessage(error); + } + + show(args: any[]): boolean { + if (super.show(args)) { + const config = args[0] as ModalConfig; + + const originalRegistrationAction = this.submitAction; + this.submitAction = (_) => { + // Prevent overlapping overrides on action modification + this.submitAction = originalRegistrationAction; + this.sanitizeInputs(); + this.scene.ui.setMode(Mode.LOADING, { buttonActions: [] }); + const onFail = error => { + this.scene.ui.setMode(Mode.REGISTRATION_FORM, Object.assign(config, { errorMessage: error?.trim() })); + this.scene.ui.playError(); + }; + if (!this.inputs[0].text) + return onFail('Username must not be empty'); + if (!this.inputs[1].text) + return onFail(this.getReadableErrorMessage('invalid password')); + if (this.inputs[1].text !== this.inputs[2].text) + return onFail('Password must match confirm password'); + Utils.apiPost('account/register', JSON.stringify({ username: this.inputs[0].text, password: this.inputs[1].text })) + .then(response => response.text()) + .then(response => { + if (!response) { + Utils.apiPost('account/login', JSON.stringify({ username: this.inputs[0].text, password: this.inputs[1].text })) + .then(response => { + if (!response.ok) + return response.text(); + return response.json(); + }) + .then(response => { + if (response.hasOwnProperty('token')) { + Utils.setCookie(Utils.sessionIdKey, response.token); + originalRegistrationAction(); + } else + onFail(response); + }); + } else + onFail(response); + }); + }; + + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/src/ui/settings-ui-handler.ts b/src/ui/settings-ui-handler.ts index 9bc889febae..608de5dbbe1 100644 --- a/src/ui/settings-ui-handler.ts +++ b/src/ui/settings-ui-handler.ts @@ -87,7 +87,7 @@ export default class SettingsUiHandler extends UiHandler { this.settingsContainer.setVisible(false); } - show(args: any[]) { + show(args: any[]): boolean { super.show(args); const settings: object = localStorage.hasOwnProperty('settings') ? JSON.parse(localStorage.getItem('settings')) : {}; @@ -100,6 +100,8 @@ export default class SettingsUiHandler extends UiHandler { this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1); this.getUi().hideTooltip(); + + return true; } processInput(button: Button): boolean { diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 7a31f7df778..cef910b7956 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -314,7 +314,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.updateInstructions(); } - show(args: any[]): void { + show(args: any[]): boolean { if (args.length >= 1 && args[0] instanceof Function) { super.show(args); @@ -335,7 +335,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.setCursor(0); this.setGenMode(true); this.setCursor(0); + + return true; } + + return false; } showText(text: string, delay?: integer, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer) { diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index b6645a778da..6bfb8198634 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -181,7 +181,7 @@ export default class SummaryUiHandler extends UiHandler { return `summary_${Page[page].toLowerCase()}`; } - show(args: any[]) { + show(args: any[]): boolean { super.show(args); this.pokemon = args[0] as PlayerPokemon; @@ -237,6 +237,8 @@ export default class SummaryUiHandler extends UiHandler { this.status.setFrame(this.pokemon.status ? StatusEffect[this.pokemon.status.effect].toLowerCase() : 'pokerus'); } else this.hideStatus(!fromSummary); + + return true; } processInput(button: Button): boolean { diff --git a/src/ui/target-select-ui-handler.ts b/src/ui/target-select-ui-handler.ts index 9a1b9436692..96cff1716b6 100644 --- a/src/ui/target-select-ui-handler.ts +++ b/src/ui/target-select-ui-handler.ts @@ -23,9 +23,9 @@ export default class TargetSelectUiHandler extends UiHandler { setup(): void { } - show(args: any[]) { + show(args: any[]): boolean { if (args.length < 3) - return; + return false; super.show(args); @@ -36,9 +36,11 @@ export default class TargetSelectUiHandler extends UiHandler { this.targets = getMoveTargets(this.scene.getPlayerField()[this.fieldIndex], this.move).targets; if (!this.targets.length) - return; + return false; this.setCursor(this.targets.indexOf(this.cursor) > -1 ? this.cursor : this.targets[0]); + + return true; } processInput(button: Button): boolean { diff --git a/src/ui/text.ts b/src/ui/text.ts index 43942c6aa3e..927fe93899f 100644 --- a/src/ui/text.ts +++ b/src/ui/text.ts @@ -1,4 +1,5 @@ import BBCodeText from "phaser3-rex-plugins/plugins/gameobjects/tagtext/bbcodetext/BBCodeText"; +import InputText from "phaser3-rex-plugins/plugins/inputtext"; export enum TextStyle { MESSAGE, @@ -22,7 +23,7 @@ export function addTextObject(scene: Phaser.Scene, x: number, y: number, content const ret = scene.add.text(x, y, content, styleOptions); ret.setScale(0.1666666667); ret.setShadow(shadowSize, shadowSize, shadowColor); - if (!styleOptions.lineSpacing) + if (!(styleOptions as Phaser.Types.GameObjects.Text.TextStyle).lineSpacing) ret.setLineSpacing(5); return ret; @@ -35,13 +36,23 @@ export function addBBCodeTextObject(scene: Phaser.Scene, x: number, y: number, c scene.add.existing(ret); ret.setScale(0.1666666667); ret.setShadow(shadowSize, shadowSize, shadowColor); - if (!styleOptions.lineSpacing) + if (!(styleOptions as Phaser.Types.GameObjects.Text.TextStyle).lineSpacing) ret.setLineSpacing(5); return ret; } -function getTextStyleOptions(style: TextStyle, extraStyleOptions?: Phaser.Types.GameObjects.Text.TextStyle): [ Phaser.Types.GameObjects.Text.TextStyle, string, integer ] { +export function addTextInputObject(scene: Phaser.Scene, x: number, y: number, width: number, height: number, style: TextStyle, extraStyleOptions?: InputText.IConfig): InputText { + const [ styleOptions ] = getTextStyleOptions(style, extraStyleOptions); + + const ret = new InputText(scene, x, y, width, height, styleOptions as InputText.IConfig); + scene.add.existing(ret); + ret.setScale(0.1666666667); + + return ret; +} + +function getTextStyleOptions(style: TextStyle, extraStyleOptions?: Phaser.Types.GameObjects.Text.TextStyle): [ Phaser.Types.GameObjects.Text.TextStyle | InputText.IConfig, string, integer ] { let shadowColor: string; let shadowSize = 6; diff --git a/src/ui/ui-handler.ts b/src/ui/ui-handler.ts index 5c66b69f316..16c7b375643 100644 --- a/src/ui/ui-handler.ts +++ b/src/ui/ui-handler.ts @@ -14,8 +14,10 @@ export default abstract class UiHandler { abstract setup(): void; - show(_args: any[]): void { + show(_args: any[]): boolean { this.active = true; + + return true; } abstract processInput(button: Button): boolean; diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 0271c93a7c9..9ab20669f97 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -24,8 +24,11 @@ import EggHatchSceneHandler from './egg-hatch-scene-handler'; import EggListUiHandler from './egg-list-ui-handler'; import EggGachaUiHandler from './egg-gacha-ui-handler'; import VouchersUiHandler from './vouchers-ui-handler'; -import VoucherBar from './voucher-bar'; import { addWindow } from './window'; +import LoginFormUiHandler from './login-form-ui-handler'; +import RegistrationFormUiHandler from './registration-form-ui-handler'; +import LoadingModalUiHandler from './loading-modal-ui-handler'; +import * as Utils from "../utils"; export enum Mode { MESSAGE, @@ -48,7 +51,10 @@ export enum Mode { ACHIEVEMENTS, VOUCHERS, EGG_LIST, - EGG_GACHA + EGG_GACHA, + LOGIN_FORM, + REGISTRATION_FORM, + LOADING }; const transitionModes = [ @@ -68,7 +74,10 @@ const noTransitionModes = [ Mode.MENU, Mode.SETTINGS, Mode.ACHIEVEMENTS, - Mode.VOUCHERS + Mode.VOUCHERS, + Mode.LOGIN_FORM, + Mode.REGISTRATION_FORM, + Mode.LOADING ]; export default class UI extends Phaser.GameObjects.Container { @@ -77,7 +86,6 @@ export default class UI extends Phaser.GameObjects.Container { private handlers: UiHandler[]; private overlay: Phaser.GameObjects.Rectangle; public achvBar: AchvBar; - public voucherBar: VoucherBar; private tooltipContainer: Phaser.GameObjects.Container; private tooltipBg: Phaser.GameObjects.NineSlice; @@ -112,7 +120,10 @@ export default class UI extends Phaser.GameObjects.Container { new AchvsUiHandler(scene), new VouchersUiHandler(scene), new EggListUiHandler(scene), - new EggGachaUiHandler(scene) + new EggGachaUiHandler(scene), + new LoginFormUiHandler(scene), + new RegistrationFormUiHandler(scene), + new LoadingModalUiHandler(scene) ]; } @@ -135,7 +146,7 @@ export default class UI extends Phaser.GameObjects.Container { this.tooltipContainer = this.scene.add.container(0, 0); this.tooltipContainer.setVisible(false); - this.tooltipBg = addWindow(this.scene, 0, 0, 128, 31); + this.tooltipBg = addWindow(this.scene as BattleScene, 0, 0, 128, 31); this.tooltipBg.setOrigin(0, 0); this.tooltipTitle = addTextObject(this.scene, 64, 4, '', TextStyle.TOOLTIP_TITLE); @@ -323,7 +334,7 @@ export default class UI extends Phaser.GameObjects.Container { revertMode(): Promise { return new Promise(resolve => { - if (!this.modeChain.length) + if (!this?.modeChain?.length) return resolve(false); const lastMode = this.mode; @@ -349,4 +360,12 @@ export default class UI extends Phaser.GameObjects.Container { resolve(true); }); } + + revertModes(): Promise { + return new Promise(resolve => { + if (!this?.modeChain?.length) + return resolve(); + this.revertMode().then(success => Utils.executeIf(success, this.revertModes).then(() => resolve())); + }); + } } \ No newline at end of file diff --git a/src/ui/vouchers-ui-handler.ts b/src/ui/vouchers-ui-handler.ts index 7a84343c872..d986b4dbd8a 100644 --- a/src/ui/vouchers-ui-handler.ts +++ b/src/ui/vouchers-ui-handler.ts @@ -95,7 +95,7 @@ export default class VouchersUiHandler extends MessageUiHandler { this.vouchersContainer.setVisible(false); } - show(args: any[]) { + show(args: any[]): boolean { super.show(args); const voucherUnlocks = this.scene.gameData.voucherUnlocks; @@ -117,6 +117,8 @@ export default class VouchersUiHandler extends MessageUiHandler { this.getUi().moveTo(this.vouchersContainer, this.getUi().length - 1); this.getUi().hideTooltip(); + + return true; } protected showVoucher(voucher: Voucher) { diff --git a/src/ui/window.ts b/src/ui/window.ts index 92a04297cbc..c0299e9be68 100644 --- a/src/ui/window.ts +++ b/src/ui/window.ts @@ -1,5 +1,22 @@ import BattleScene from "../battle-scene"; +export enum WindowVariant { + NORMAL, + THIN, + XTHIN +} + +export function getWindowVariantSuffix(windowVariant: WindowVariant): string { + switch (windowVariant) { + case WindowVariant.THIN: + return '_thin'; + case WindowVariant.XTHIN: + return '_xthin'; + default: + return ''; + } +} + const windowTypeControlColors = { 0: [ '#706880', '#8888c8', '#484868' ], 1: [ '#d04028', '#e0a028', '#902008' ], @@ -7,8 +24,11 @@ const windowTypeControlColors = { 3: [ '#2068d0', '#80b0e0', '#104888' ] }; -export function addWindow(scene: BattleScene, x: number, y: number, width: number, height: number, mergeMaskTop?: boolean, mergeMaskLeft?: boolean, maskOffsetX?: number, maskOffsetY?: number): Phaser.GameObjects.NineSlice { - const window = scene.add.nineslice(x, y, `window_${scene.windowType}`, null, width, height, 8, 8, 8, 8); +export function addWindow(scene: BattleScene, x: number, y: number, width: number, height: number, mergeMaskTop?: boolean, mergeMaskLeft?: boolean, maskOffsetX?: number, maskOffsetY?: number, windowVariant?: WindowVariant): Phaser.GameObjects.NineSlice { + if (windowVariant === undefined) + windowVariant = WindowVariant.NORMAL; + + const window = scene.add.nineslice(x, y, `window_${scene.windowType}${getWindowVariantSuffix(windowVariant)}`, null, width, height, 6, 6, 6, 6); window.setOrigin(0, 0); if (mergeMaskTop || mergeMaskLeft) { @@ -24,7 +44,7 @@ export function addWindow(scene: BattleScene, x: number, y: number, width: numbe } export function updateWindowType(scene: BattleScene, windowTypeIndex: integer): void { - const windowObjects: Phaser.GameObjects.NineSlice[] = []; + const windowObjects: [Phaser.GameObjects.NineSlice, WindowVariant][] = []; const traverse = (object: any) => { if (object.hasOwnProperty('children')) { const children = object.children as Phaser.GameObjects.DisplayList; @@ -35,7 +55,7 @@ export function updateWindowType(scene: BattleScene, windowTypeIndex: integer): traverse(child); } else if (object instanceof Phaser.GameObjects.NineSlice) { if (object.texture.key.startsWith('window_')) - windowObjects.push(object); + windowObjects.push([ object, object.texture.key.endsWith(getWindowVariantSuffix(WindowVariant.XTHIN)) ? WindowVariant.XTHIN : object.texture.key.endsWith(getWindowVariantSuffix(WindowVariant.THIN)) ? WindowVariant.THIN : WindowVariant.NORMAL ]); } } @@ -48,6 +68,6 @@ export function updateWindowType(scene: BattleScene, windowTypeIndex: integer): const windowKey = `window_${windowTypeIndex}`; - for (let window of windowObjects) - window.setTexture(windowKey); + for (let [ window, variant ] of windowObjects) + window.setTexture(`${windowKey}${getWindowVariantSuffix(variant)}`); } \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index e445a399610..c51ae2b089c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -130,6 +130,55 @@ export function executeIf(condition: boolean, promiseFunc: () => Promise): return condition ? promiseFunc() : new Promise(resolve => resolve(null)); } +export const sessionIdKey = 'pokerogue_sessionId'; +export const isLocal = window.location.hostname === 'localhost'; +export const serverUrl = isLocal ? 'http://localhost:8001' : ''; +export const apiUrl = isLocal ? serverUrl : 'api'; + +export function setCookie(cName: string, cValue: string): void { + document.cookie = `${cName}=${cValue};SameSite=Strict;path=/`; +} + +export function getCookie(cName: string): string { + const name = `${cName}=`; + const ca = document.cookie.split(';'); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === ' ') + c = c.substring(1); + if (c.indexOf(name) === 0) + return c.substring(name.length, c.length); + } + return ''; +} + +export function apiFetch(path: string): Promise { + return new Promise((resolve, reject) => { + const sId = getCookie(sessionIdKey); + const headers = sId ? { 'Authorization': sId } : {}; + fetch(`${apiUrl}/${path}`, { headers: headers }) + .then(response => resolve(response)) + .catch(err => reject(err)); + }); +} + +export function apiPost(path: string, data?: any, contentType?: string): Promise { + if (!contentType) + contentType = 'application/json'; + return new Promise((resolve, reject) => { + const headers = { + 'Accept': contentType, + 'Content-Type': contentType, + }; + const sId = getCookie(sessionIdKey); + if (sId) + headers['Authorization'] = sId; + fetch(`${apiUrl}/${path}`, { method: 'POST', headers: headers, body: data }) + .then(response => resolve(response)) + .catch(err => reject(err)); + }); +} + export class BooleanHolder { public value: boolean;