Add tutorial framework (WiP)

pull/14/head
Flashfyre 2024-02-13 18:42:11 -05:00
parent c231886d5f
commit bf18b1ceb1
8 changed files with 189 additions and 36 deletions

View File

@ -50,6 +50,7 @@ import { SpeciesFormChangeActiveTrigger, SpeciesFormChangeManualTrigger, Species
import { battleSpecDialogue } from "./data/dialogue";
import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "./ui/modifier-select-ui-handler";
import { Setting } from "./system/settings";
import { Tutorial, handleTutorial } from "./tutorial";
export class LoginPhase extends BattlePhase {
private showText: boolean;
@ -105,7 +106,7 @@ export class LoginPhase extends BattlePhase {
end(): void {
this.scene.ui.setMode(Mode.MESSAGE);
super.end();
handleTutorial(this.scene, Tutorial.Intro).then(() => super.end());
}
}
@ -585,7 +586,7 @@ export class EncounterPhase extends BattlePhase {
message = this.scene.currentBattle.trainer.config.encounterMessages[trainer.female ? 1 : 0];
else
this.scene.executeWithSeedOffset(() => message = Phaser.Math.RND.pick(this.scene.currentBattle.trainer.config.encounterMessages), this.scene.currentBattle.waveIndex);
this.scene.ui.showDialogue(message, trainer.getName(), null, doSummon, null, true);
this.scene.ui.showDialogue(message, trainer.getName(), null, doSummon);
}
}
}
@ -643,7 +644,7 @@ export class EncounterPhase extends BattlePhase {
this.scene.ui.showText(this.getEncounterMessage(), null, () => {
this.scene.ui.showDialogue(battleSpecDialogue[BattleSpec.FINAL_BOSS].encounter, enemy.name, null, () => {
this.doEncounterCommon(false);
}, null, true);
});
}, 1500, true);
return true;
}
@ -2504,7 +2505,7 @@ export class DamagePhase extends PokemonPhase {
}
super.end();
}, null, true);
});
return;
}
break;
@ -2605,7 +2606,7 @@ export class FaintPhase extends PokemonPhase {
if (!this.player) {
const enemy = this.getPokemon();
if (enemy.formIndex) {
this.scene.ui.showDialogue(battleSpecDialogue[BattleSpec.FINAL_BOSS].secondStageWin, enemy.name, null, () => this.doFaint(), null, true);
this.scene.ui.showDialogue(battleSpecDialogue[BattleSpec.FINAL_BOSS].secondStageWin, enemy.name, null, () => this.doFaint());
return true;
}
}
@ -2759,7 +2760,7 @@ export class TrainerVictoryPhase extends BattlePhase {
for (let p = messagePages.length - 1; p >= 0; p--) {
const originalFunc = showMessageAndEnd;
showMessageAndEnd = () => this.scene.ui.showDialogue(messagePages[p], this.scene.currentBattle.trainer.getName(), null, originalFunc, null, true);
showMessageAndEnd = () => this.scene.ui.showDialogue(messagePages[p], this.scene.currentBattle.trainer.getName(), null, originalFunc);
}
}
showMessageAndEnd();

View File

@ -94,6 +94,7 @@ export default class BattleScene extends Phaser.Scene {
public seVolume: number = 1;
public gameSpeed: integer = 1;
public showLevelUpStats: boolean = true;
public enableTutorials: boolean = true;
public windowType: integer = 1;
public enableTouchControls: boolean = false;
public enableVibration: boolean = false;

View File

@ -22,13 +22,15 @@ import { Mode } from "../ui/ui";
import { loggedInUser, updateUserInfo } from "../account";
import { Nature } from "../data/nature";
import { GameStats } from "./game-stats";
import { Tutorial } from "../tutorial";
const saveKey = 'x0i2O7WRiANTqPmZ'; // Temporary; secure encryption is not yet necessary
export enum GameDataType {
SYSTEM,
SESSION,
SETTINGS
SETTINGS,
TUTORIALS
}
export enum PlayerGender {
@ -45,6 +47,8 @@ export function getDataTypeKey(dataType: GameDataType): string {
return 'sessionData';
case GameDataType.SETTINGS:
return 'settings';
case GameDataType.TUTORIALS:
return 'tutorials';
}
}
@ -129,6 +133,10 @@ export interface DexAttrProps {
formIndex: integer;
}
export interface TutorialFlags {
[key: string]: boolean
}
const systemShortKeys = {
seenAttr: '$sa',
caughtAttr: '$ca',
@ -366,6 +374,39 @@ export class GameData {
setSetting(this.scene, setting as Setting, settings[setting]);
}
public saveTutorialFlag(tutorial: Tutorial, flag: boolean): boolean {
let tutorials: object = {};
if (localStorage.hasOwnProperty('tutorials'))
tutorials = JSON.parse(localStorage.getItem('tutorials'));
Object.keys(Tutorial).map(t => t as Tutorial).forEach(t => {
const key = Tutorial[t];
if (key === tutorial)
tutorials[key] = flag;
else
tutorials[key] ??= false;
});
localStorage.setItem('tutorials', JSON.stringify(tutorials));
return true;
}
public getTutorialFlags(): TutorialFlags {
const ret: TutorialFlags = {};
Object.values(Tutorial).map(tutorial => tutorial as Tutorial).forEach(tutorial => ret[Tutorial[tutorial]] = false);
if (!localStorage.hasOwnProperty('tutorials'))
return ret;
const tutorials = JSON.parse(localStorage.getItem('tutorials'));
for (let tutorial of Object.keys(tutorials))
ret[tutorial] = tutorials[tutorial];
return ret;
}
saveSession(scene: BattleScene, skipVerification?: boolean): Promise<boolean> {
return new Promise<boolean>(resolve => {
Utils.executeIf(!skipVerification, updateUserInfo).then(success => {
@ -582,7 +623,7 @@ export class GameData {
link.click();
link.remove();
};
if (!bypassLogin && dataType !== GameDataType.SETTINGS) {
if (!bypassLogin && dataType < GameDataType.SETTINGS) {
Utils.apiFetch(`savedata/get?datatype=${dataType}`)
.then(response => response.text())
.then(response => {
@ -629,6 +670,7 @@ export class GameData {
valid = !!sessionData.party && !!sessionData.enemyParty && !!sessionData.timestamp;
break;
case GameDataType.SETTINGS:
case GameDataType.TUTORIALS:
valid = true;
break;
}
@ -647,6 +689,9 @@ export class GameData {
case GameDataType.SETTINGS:
dataName = 'settings';
break;
case GameDataType.TUTORIALS:
dataName = 'tutorials';
break;
}
const displayError = (error: string) => this.scene.ui.showText(error, null, () => this.scene.ui.showText(null, 0), Utils.fixedInt(1500));
@ -655,7 +700,7 @@ export class GameData {
return this.scene.ui.showText(`Your ${dataName} data could not be loaded. It may be corrupted.`, null, () => this.scene.ui.showText(null, 0), Utils.fixedInt(1500));
this.scene.ui.showText(`Your ${dataName} data will be overridden and the page will reload. Proceed?`, null, () => {
this.scene.ui.setOverlayMode(Mode.CONFIRM, () => {
if (!bypassLogin && dataType !== GameDataType.SETTINGS) {
if (!bypassLogin && dataType < GameDataType.SETTINGS) {
updateUserInfo().then(success => {
if (!success)
return displayError(`Could not contact the server. Your ${dataName} data could not be imported.`);

View File

@ -9,6 +9,7 @@ export enum Setting {
SE_Volume = "SE_VOLUME",
Show_Stats_on_Level_Up = "SHOW_LEVEL_UP_STATS",
Window_Type = "WINDOW_TYPE",
Tutorials = "TUTORIALS",
Player_Gender = "PLAYER_GENDER",
Touch_Controls = "TOUCH_CONTROLS",
Vibration = "VIBRATION"
@ -29,6 +30,7 @@ export const settingOptions: SettingOptions = {
[Setting.SE_Volume]: new Array(11).fill(null).map((_, i) => i ? (i * 10).toString() : 'Mute'),
[Setting.Show_Stats_on_Level_Up]: [ 'Off', 'On' ],
[Setting.Window_Type]: new Array(4).fill(null).map((_, i) => (i + 1).toString()),
[Setting.Tutorials]: [ 'Off', 'On' ],
[Setting.Player_Gender]: [ 'Boy', 'Girl' ],
[Setting.Touch_Controls]: [ 'Auto', 'Disabled' ],
[Setting.Vibration]: [ 'Auto', 'Disabled' ]
@ -41,6 +43,7 @@ export const settingDefaults: SettingDefaults = {
[Setting.SE_Volume]: 10,
[Setting.Show_Stats_on_Level_Up]: 1,
[Setting.Window_Type]: 0,
[Setting.Tutorials]: 1,
[Setting.Player_Gender]: 0,
[Setting.Touch_Controls]: 0,
[Setting.Vibration]: 0
@ -69,6 +72,9 @@ export function setSetting(scene: BattleScene, setting: Setting, value: integer)
case Setting.Window_Type:
updateWindowType(scene, parseInt(settingOptions[setting][value]));
break;
case Setting.Tutorials:
scene.enableTutorials = settingOptions[setting][value] === 'On';
break;
case Setting.Player_Gender:
if (scene.gameData) {
const female = settingOptions[setting][value] === 'Girl';

50
src/tutorial.ts Normal file
View File

@ -0,0 +1,50 @@
import BattleScene from "./battle-scene";
export enum Tutorial {
Intro = "INTRO",
Menu = "MENU",
Starter_Select = "STARTER_SELECT",
Select_Item = "SELECT_ITEM",
Gacha = "GACHA",
Egg_List = "EGG_LIST"
}
const tutorialHandlers = {
[Tutorial.Intro]: (scene: BattleScene) => {
return new Promise<void>(resolve => {
scene.ui.showText(`Welcome to PokéRogue! This is a battle-focused Pokémon fangame with roguelite elements.
$This game is not monetized and we claim no ownership of Pokémon nor of the copyrighted assets used.
$The game is a work in progress, but fully playable.\nFor bug reports, please use the Discord community.`, null, () => resolve());
});
},
[Tutorial.Menu]: (scene: BattleScene) => {
return new Promise<void>(resolve => {
if (scene.enableTouchControls)
return resolve();
scene.ui.showText(`To access the menu, press M or Escape. The menu contains settings and various features.`, null, () => resolve());
});
},
[Tutorial.Starter_Select]: (scene: BattleScene) => {
return new Promise<void>(resolve => {
scene.ui.showText(`From this screen, you can select the starters for your party.
$Each starter has a value. Your party can have up to 6 members as long as the total does not exceed 10.
$You can also select gender, ability, and form depending on the variants you've caught or hatched.
$The IVs for a species are also the best of every one you've caught, so try to get lots of the same species!`, null, () => resolve());
});
},
};
export function handleTutorial(scene: BattleScene, tutorial: Tutorial): Promise<boolean> {
return new Promise<boolean>(resolve => {
if (!scene.enableTutorials)
return resolve(false);
if (scene.gameData.getTutorialFlags()[tutorial])
return resolve(false);
tutorialHandlers[tutorial](scene).then(() => {
scene.gameData.saveTutorialFlag(tutorial, true);
resolve(true);
});
});
}

View File

@ -9,10 +9,13 @@ export default class SettingsUiHandler extends UiHandler {
private settingsContainer: Phaser.GameObjects.Container;
private optionsContainer: Phaser.GameObjects.Container;
private scrollCursor: integer;
private optionsBg: Phaser.GameObjects.NineSlice;
private optionCursors: integer[];
private settingLabels: Phaser.GameObjects.Text[];
private optionValueLabels: Phaser.GameObjects.Text[][];
private cursorObj: Phaser.GameObjects.NineSlice;
@ -40,14 +43,14 @@ export default class SettingsUiHandler extends UiHandler {
this.optionsContainer = this.scene.add.container(0, 0);
const settingLabels = [];
this.settingLabels = [];
this.optionValueLabels = [];
Object.keys(Setting).forEach((setting, s) => {
settingLabels[s] = addTextObject(this.scene, 8, 28 + s * 16, setting.replace(/\_/g, ' '), TextStyle.SETTINGS_LABEL);
settingLabels[s].setOrigin(0, 0);
this.settingLabels[s] = addTextObject(this.scene, 8, 28 + s * 16, setting.replace(/\_/g, ' '), TextStyle.SETTINGS_LABEL);
this.settingLabels[s].setOrigin(0, 0);
this.optionsContainer.add(settingLabels[s]);
this.optionsContainer.add(this.settingLabels[s]);
this.optionValueLabels.push(settingOptions[Setting[setting]].map((option, o) => {
const valueLabel = addTextObject(this.scene, 0, 0, option, settingDefaults[Setting[setting]] === o ? TextStyle.SETTINGS_SELECTED : TextStyle.WINDOW);
@ -60,7 +63,7 @@ export default class SettingsUiHandler extends UiHandler {
const totalWidth = this.optionValueLabels[s].map(o => o.width).reduce((total, width) => total += width, 0);
const labelWidth = Math.max(78, settingLabels[s].displayWidth + 8);
const labelWidth = Math.max(78, this.settingLabels[s].displayWidth + 8);
const totalSpace = (300 - labelWidth) - totalWidth / 6;
const optionSpacing = Math.floor(totalSpace / (this.optionValueLabels[s].length - 1));
@ -68,7 +71,7 @@ export default class SettingsUiHandler extends UiHandler {
let xOffset = 0;
for (let value of this.optionValueLabels[s]) {
value.setPositionRelative(settingLabels[s], labelWidth + xOffset, 0);
value.setPositionRelative(this.settingLabels[s], labelWidth + xOffset, 0);
xOffset += value.width / 6 + optionSpacing;
}
});
@ -83,6 +86,7 @@ export default class SettingsUiHandler extends UiHandler {
ui.add(this.settingsContainer);
this.setCursor(0);
this.setScrollCursor(0);
this.settingsContainer.setVisible(false);
}
@ -113,22 +117,31 @@ export default class SettingsUiHandler extends UiHandler {
success = true;
this.scene.ui.revertMode();
} else {
const cursor = this.cursor + this.scrollCursor;
switch (button) {
case Button.UP:
if (this.cursor)
success = this.setCursor(this.cursor - 1);
if (cursor) {
if (this.cursor)
success = this.setCursor(this.cursor - 1);
else
success = this.setScrollCursor(this.scrollCursor - 1);
}
break;
case Button.DOWN:
if (this.cursor < this.optionValueLabels.length - 1)
success = this.setCursor(this.cursor + 1);
if (cursor < this.optionValueLabels.length) {
if (this.cursor < 8)
success = this.setCursor(this.cursor + 1);
else if (this.scrollCursor < this.optionValueLabels.length - 9)
success = this.setScrollCursor(this.scrollCursor + 1);
}
break;
case Button.LEFT:
if (this.optionCursors[this.cursor])
success = this.setOptionCursor(this.cursor, this.optionCursors[this.cursor] - 1, true);
if (this.optionCursors[cursor])
success = this.setOptionCursor(cursor, this.optionCursors[cursor] - 1, true);
break;
case Button.RIGHT:
if (this.optionCursors[this.cursor] < this.optionValueLabels[this.cursor].length - 1)
success = this.setOptionCursor(this.cursor, this.optionCursors[this.cursor] + 1, true);
if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1)
success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true);
break;
}
}
@ -148,7 +161,7 @@ export default class SettingsUiHandler extends UiHandler {
this.optionsContainer.add(this.cursorObj);
}
this.cursorObj.setPositionRelative(this.optionsBg, 4, 4 + this.cursor * 16);
this.cursorObj.setPositionRelative(this.optionsBg, 4, 4 + (this.cursor + this.scrollCursor) * 16);
return ret;
}
@ -179,6 +192,30 @@ export default class SettingsUiHandler extends UiHandler {
return true;
}
setScrollCursor(scrollCursor: integer): boolean {
if (scrollCursor === this.scrollCursor)
return false;
this.scrollCursor = scrollCursor;
this.updateSettingsScroll();
this.setCursor(this.cursor);
return true;
}
updateSettingsScroll(): void {
this.optionsContainer.setY(-16 * this.scrollCursor);
for (let s = 0; s < this.settingLabels.length; s++) {
const visible = s >= this.scrollCursor && s < this.scrollCursor + 9;
this.settingLabels[s].setVisible(visible);
for (let option of this.optionValueLabels[s])
option.setVisible(visible);
}
}
clear() {
super.clear();
this.settingsContainer.setVisible(false);

View File

@ -17,6 +17,7 @@ import { addWindow } from "./window";
import { Nature, getNatureName } from "../data/nature";
import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
import { pokemonFormChanges } from "../data/pokemon-forms";
import { Tutorial, handleTutorial } from "../tutorial";
export type StarterSelectCallback = (starters: Starter[]) => void;
@ -365,6 +366,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.setGenMode(true);
this.setCursor(0);
//handleTutorial(this.scene, Tutorial.Starter_Select);
return true;
}

View File

@ -178,28 +178,38 @@ export default class UI extends Phaser.GameObjects.Container {
}
showText(text: string, delay?: integer, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer): void {
const handler = this.getHandler();
if (handler instanceof MessageUiHandler)
(handler as MessageUiHandler).showText(text, delay, callback, callbackDelay, prompt, promptDelay);
else
this.getMessageHandler().showText(text, delay, callback, callbackDelay, prompt, promptDelay);
}
showDialogue(text: string, name: string, delay: integer = 0, callback: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer): void {
if (text.indexOf('$') > -1) {
if (prompt && text.indexOf('$') > -1) {
const messagePages = text.split(/\$/g).map(m => m.trim());
let showMessageAndCallback = () => callback();
for (let p = messagePages.length - 1; p >= 0; p--) {
const originalFunc = showMessageAndCallback;
showMessageAndCallback = () => this.showDialogue(messagePages[p], name, null, originalFunc, null, true);
showMessageAndCallback = () => this.showText(messagePages[p], null, originalFunc, null, true);
}
showMessageAndCallback();
} else {
const handler = this.getHandler();
if (handler instanceof MessageUiHandler)
(handler as MessageUiHandler).showDialogue(text, name, delay, callback, callbackDelay, prompt, promptDelay);
(handler as MessageUiHandler).showText(text, delay, callback, callbackDelay, prompt, promptDelay);
else
this.getMessageHandler().showDialogue(text, name, delay, callback, callbackDelay, prompt, promptDelay);
this.getMessageHandler().showText(text, delay, callback, callbackDelay, prompt, promptDelay);
}
}
showDialogue(text: string, name: string, delay: integer = 0, callback: Function, callbackDelay?: integer, promptDelay?: integer): void {
if (text.indexOf('$') > -1) {
const messagePages = text.split(/\$/g).map(m => m.trim());
let showMessageAndCallback = () => callback();
for (let p = messagePages.length - 1; p >= 0; p--) {
const originalFunc = showMessageAndCallback;
showMessageAndCallback = () => this.showDialogue(messagePages[p], name, null, originalFunc);
}
showMessageAndCallback();
} else {
const handler = this.getHandler();
if (handler instanceof MessageUiHandler)
(handler as MessageUiHandler).showDialogue(text, name, delay, callback, callbackDelay, true, promptDelay);
else
this.getMessageHandler().showDialogue(text, name, delay, callback, callbackDelay, true, promptDelay);
}
}