123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410 |
- import * as audio from '../audio';
- import * as display from '../display';
- import * as level from '../level';
- import * as kana from '../kana';
- import * as util from '../util';
- import * as youtube from '../youtube';
- import { GameContext, Screen, ScreenManager } from './common';
- import Level = level.Level;
- class TypingScreenContext {
- track: audio.Track | null = null;
- constructor(
- readonly context: GameContext,
- readonly level: Level,
- readonly switchClosure: (screen: Screen | null) => void
- ) {}
- get container() {
- return this.context.container;
- }
- get audioManager() {
- return this.context.audioManager;
- }
- get bgManager() {
- return this.context.bgManager;
- }
- switchScreen(screen: Screen | null): void {
- this.switchClosure(screen);
- }
- }
- export class TypingScreen extends ScreenManager implements Screen {
- readonly name: string = 'game';
- constructor(
- readonly context: GameContext,
- readonly level: Level,
- readonly prevScreen: Screen
- ) {
- super(context.container);
- }
- enter(): void {
- if (this.level.background) {
- this.context.bgManager.setBackground(this.level.background);
- }
- let context = new TypingScreenContext(this.context, this.level, (screen) =>
- this.switchScreen(screen)
- );
- let loadingScreen = new TypingLoadingScreen(context);
- this.switchScreen(loadingScreen);
- }
- handleInput(key: string): void {
- if (this.activeScreen !== null) {
- this.activeScreen.handleInput(key);
- }
- }
- switchScreen(screen: Screen | null): void {
- super.switchScreen(screen);
- if (screen == null) {
- this.context.switchScreen(this.prevScreen);
- }
- }
- exit(): void {}
- transitionExit(): void {}
- }
- class TypingLoadingScreen implements Screen {
- readonly name: string = 'game-loading';
- barElement: HTMLElement | null = null;
- textElement: HTMLElement | null = null;
- readyElement: HTMLElement | null = null;
- isReady: boolean = false;
- fnContext: util.FnContext = new util.FnContext();
- constructor(readonly context: TypingScreenContext) {}
- enter(): void {
- let loader: HTMLElement = util.getElement(
- this.context.container,
- '#loader'
- );
- this.readyElement = util.getElement(this.context.container, '#ready');
- if (this.context.level.audio != null) {
- loader.style.visibility = 'visible';
- this.barElement = util.getElement(loader, '.progress-bar .shade');
- this.textElement = util.getElement(loader, '.label');
- this.barElement.style.width = '0%';
- this.textElement.textContent = 'music loading';
- this.readyElement.querySelector('.status')!.textContent = 'Loading';
- this.readyElement.querySelector('.message')!.textContent = 'please wait';
- this.fnContext.invalidate();
- const videoId = youtube.getVideoId(this.context.level.audio);
- const progressListener = this.fnContext.wrap((percentage: number) => {
- this.barElement!.style.width = `${percentage}%`;
- });
- let trackPromise: Promise<audio.Track>;
- if (videoId !== null) {
- const ytElement = document.createElement('div');
- trackPromise = this.context.audioManager.loadTrackFromYoutube(
- videoId,
- ytElement,
- progressListener
- );
- this.context.bgManager.setVideo(ytElement);
- if (this.context.level.background == undefined) {
- trackPromise.then((track) => {
- track.addListener((_, state) => {
- if (state === audio.PlayState.PLAYING) {
- this.context.bgManager.showVideo();
- }
- });
- });
- }
- } else {
- trackPromise = this.context.audioManager.loadTrackWithProgress(
- this.context.level.audio,
- progressListener
- );
- }
- trackPromise.then(
- this.fnContext.wrap((track: audio.Track) => {
- this.context.track = track;
- this.barElement!.style.width = '100%';
- this.textElement!.textContent = 'music loaded';
- this.setReady();
- })
- );
- } else {
- loader.style.visibility = 'hidden';
- this.setReady();
- }
- }
- setReady(): void {
- this.readyElement!.querySelector('.status')!.textContent = 'Ready';
- this.readyElement!.querySelector('.message')!.textContent =
- 'press space to start';
- this.isReady = true;
- }
- handleInput(key: string): void {
- if (key === 'Escape' || key === 'Backspace') {
- this.context.switchScreen(null);
- } else if (this.isReady && (key === ' ' || key === 'Enter')) {
- this.context.switchScreen(new TypingPlayingScreen(this.context));
- }
- }
- exit(): void {
- this.fnContext.invalidate();
- }
- transitionExit(): void {
- if (this.barElement) {
- this.barElement.style.width = '0%';
- }
- }
- }
- class TypingPlayingScreen implements Screen {
- readonly name: string = 'game-playing';
- gameContainer: HTMLElement;
- currentIndex: number;
- inputState: kana.KanaInputState | null;
- isWaiting: boolean;
- skippable: boolean;
- kanjiElement: HTMLElement;
- kanaController: display.KanaDisplayController;
- romajiController: display.RomajiDisplayController;
- progressController: display.TrackProgressController | null;
- scoreController: display.ScoreController;
- lines: level.Line[];
- constructor(readonly context: TypingScreenContext) {
- this.gameContainer = util.getElement(this.context.container, '#game');
- this.currentIndex = -1;
- this.inputState = null;
- this.isWaiting = false;
- this.skippable = false;
- this.kanjiElement = util.getElement(this.gameContainer, '.kanji-line');
- this.romajiController = new display.RomajiDisplayController(
- util.getElement(this.gameContainer, '.romaji-first'),
- util.getElement(this.gameContainer, '.romaji-line')
- );
- this.kanaController = new display.KanaDisplayController(
- util.getElement(this.gameContainer, '.kana-line')
- );
- this.progressController = null;
- this.scoreController = new display.ScoreController(
- util.getElement(this.gameContainer, '.score-line'),
- util.getElement(this.gameContainer, '.stats-line')
- );
- this.lines = this.context.level.lines;
- }
- enter(): void {
- let progressElement: HTMLElement = this.gameContainer.querySelector<HTMLElement>(
- '.track-progress'
- )!;
- if (this.context.track == null) {
- progressElement.style.visibility = 'hidden';
- this.lines = this.context.level.lines.filter((line) => line.kana != '@');
- } else {
- progressElement.style.visibility = 'visible';
- const progressController = new display.TrackProgressController(
- progressElement,
- this.lines
- );
- progressController.setListener((_) => this.onIntervalEnd());
- this.context.track.addListener((track, state) => {
- if (state === audio.PlayState.PLAYING) {
- progressController.start(track.getTime());
- } else {
- progressController.pause();
- }
- });
- this.progressController = progressController;
- }
- this.onStart();
- }
- get currentLine() {
- return this.lines[this.currentIndex];
- }
- setWaiting(waiting: boolean, skippable: boolean = false): void {
- this.isWaiting = waiting;
- this.skippable = waiting && skippable;
- this.gameContainer.classList.toggle('waiting', this.isWaiting);
- this.gameContainer.classList.toggle('skippable', this.skippable);
- }
- onStart(): void {
- this.nextLine();
- if (this.context.track !== null) {
- this.context.track.start(0);
- }
- this.setWaiting(false);
- this.checkComplete();
- }
- checkComplete(): void {
- let currentLine = this.currentLine;
- if (
- currentLine != null &&
- currentLine.kana == '@' &&
- currentLine.kanji == '@'
- ) {
- this.onComplete(true);
- }
- }
- onIntervalEnd(): void {
- if (this.isWaiting) {
- this.setWaiting(false);
- } else {
- this.nextLine();
- this.scoreController.intervalEnd(false);
- }
- if (this.currentIndex >= this.lines.length) {
- this.finish();
- }
- this.checkComplete();
- }
- onComplete(autoComplete: boolean = false): void {
- this.nextLine();
- if (!autoComplete) {
- this.scoreController.intervalEnd(true);
- }
- if (this.context.track !== null) {
- // skippable if the last line was empty and the current line is longer
- // than 3 seconds
- const lastLine = this.lines[this.currentIndex - 1];
- const skippable =
- autoComplete &&
- lastLine !== undefined &&
- lastLine.end! - lastLine.start! > 3;
- this.setWaiting(true, skippable);
- } else {
- if (this.currentIndex >= this.lines.length) {
- this.finish();
- }
- }
- }
- handleInput(key: string): void {
- if (key === 'Escape' || key === 'Backspace') {
- this.finish();
- return;
- } else if (!this.isWaiting) {
- if (this.inputState !== null && /^[-_ a-z]$/.test(key)) {
- if (this.inputState.handleInput(key)) {
- this.onComplete();
- }
- }
- } else if (this.skippable && key === 'Tab' && this.context.track !== null) {
- const start = this.currentLine.start!;
- if (start - this.context.track.getTime() > 3) {
- this.context.track.start(start - 1.5);
- }
- }
- }
- nextLine(): void {
- if (this.currentIndex < this.lines.length) {
- this.currentIndex += 1;
- }
- if (this.currentIndex < this.lines.length) {
- this.setLine(this.lines[this.currentIndex]);
- } else {
- this.setLine({ kanji: '@', kana: '@' });
- }
- }
- setLine(line: level.Line): void {
- let kanji, inputState;
- if (line.kanji === '@') {
- kanji = '';
- } else {
- kanji = line.kanji;
- }
- if (line.kana === '@') {
- inputState = null;
- } else {
- inputState = new kana.KanaInputState(line.kana);
- }
- this.inputState = inputState;
- this.kanjiElement.textContent = kanji;
- this.kanaController.setInputState(this.inputState);
- this.romajiController.setInputState(this.inputState);
- this.scoreController.setInputState(this.inputState);
- }
- finish(): void {
- this.context.switchScreen(
- new TypingFinishScreen(this.context, this.scoreController.score)
- );
- }
- exit(): void {}
- transitionExit(): void {
- this.gameContainer.classList.remove('skippable');
- this.kanaController.destroy();
- this.romajiController.destroy();
- if (this.context.track !== null) {
- this.progressController!.destroy();
- this.context.track.clearListeners();
- }
- this.scoreController.destroy();
- }
- }
- class TypingFinishScreen implements Screen {
- name: string = 'game-finished';
- constructor(
- readonly context: TypingScreenContext,
- readonly score: display.Score
- ) {
- let container = this.context.container.querySelector('#score')!;
- container.querySelector('.score')!.textContent = this.score.score + '';
- container.querySelector('.max-combo')!.textContent =
- this.score.maxCombo + '';
- container.querySelector('.finished')!.textContent =
- this.score.finished + '';
- container.querySelector('.hit')!.textContent = this.score.hit + '';
- container.querySelector('.missed')!.textContent = this.score.missed + '';
- container.querySelector('.skipped')!.textContent = this.score.skipped + '';
- }
- enter(): void {}
- handleInput(key: string): void {
- if (
- key === ' ' ||
- key === 'Enter' ||
- key === 'Escape' ||
- key === 'Backspace'
- ) {
- this.context.switchScreen(null);
- }
- }
- exit(): void {}
- transitionExit(): void {
- if (this.context.track !== null) {
- this.context.track.exit();
- }
- }
- }
|