Преглед на файлове

Allow skipping long empty sections

Thomas Dy преди 3 години
родител
ревизия
fafdfa890b
променени са 7 файла, в които са добавени 68 реда и са изтрити 21 реда
  1. 10 12
      src/audio.ts
  2. 4 0
      src/game.ts
  3. 2 2
      src/game/common.ts
  4. 1 1
      src/game/loading.ts
  5. 27 5
      src/game/typing.ts
  6. 1 0
      src/index.html
  7. 23 1
      src/style.css

+ 10 - 12
src/audio.ts

@@ -98,7 +98,6 @@ export abstract class Track {
     this.listeners.forEach((l) => l(this, state));
   }
 
-  abstract play(): void;
   abstract start(fromTime?: number, duration?: number): void;
   abstract pause(): void;
   abstract stop(): void;
@@ -126,16 +125,20 @@ export class FileTrack extends Track {
     this.state = PlayState.UNSTARTED;
   }
 
+  /**
+   * Play and forget useful for SFX and the like
+   */
   play(): void {
-    this.source = this.manager.context.createBufferSource();
-    this.source.buffer = this.buffer;
-    this.source.connect(this.manager.output);
-    this.playStartTime = this.manager.getTime();
-    this.setState(PlayState.PLAYING);
-    this.source.start();
+    const source = this.manager.context.createBufferSource();
+    source.buffer = this.buffer;
+    source.connect(this.manager.output);
+    source.start();
   }
 
   start(fromTime?: number, duration?: number): void {
+    if (this.state === PlayState.PLAYING) {
+      this.stop();
+    }
     if (fromTime !== undefined) {
       this.resumeTime = fromTime;
     }
@@ -239,11 +242,6 @@ export class YoutubeTrack extends Track {
     });
   }
 
-  play(): void {
-    this.clearTimeout();
-    this.player.playVideo();
-  }
-
   start(fromTime?: number, duration?: number): void {
     this.clearTimeout();
     if (duration) {

+ 4 - 0
src/game.ts

@@ -30,6 +30,10 @@ export class MainController extends ScreenManager {
     this.loadingScreen = new LoadingScreen(gameContext, configUrl);
 
     document.addEventListener('keydown', (event) => {
+      if (event.key === 'Tab') {
+        // prevent losing focus
+        event.preventDefault();
+      }
       if (event.altKey && event.key === 'Enter') {
         polyfill.fullscreen.request(this.container);
       }

+ 2 - 2
src/game/common.ts

@@ -53,8 +53,8 @@ export class ScreenManager {
 }
 
 interface GameSounds {
-  selectSound: audio.Track | null;
-  decideSound: audio.Track | null;
+  selectSound: audio.FileTrack | null;
+  decideSound: audio.FileTrack | null;
 }
 
 export interface GameContext {

+ 1 - 1
src/game/loading.ts

@@ -53,7 +53,7 @@ export class LoadingScreen implements Screen {
     loadingElement.classList.add('finished');
   }
 
-  loadTrack(url: string): Promise<audio.Track | null> {
+  loadTrack(url: string): Promise<audio.FileTrack | null> {
     if (url == null) {
       return Promise.resolve(null);
     } else {

+ 27 - 5
src/game/typing.ts

@@ -177,6 +177,7 @@ class TypingPlayingScreen implements Screen {
   currentIndex: number;
   inputState: kana.KanaInputState | null;
   isWaiting: boolean;
+  skippable: boolean;
   kanjiElement: HTMLElement;
   kanaController: display.KanaDisplayController;
   romajiController: display.RomajiDisplayController;
@@ -189,6 +190,7 @@ class TypingPlayingScreen implements Screen {
     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'),
@@ -231,15 +233,21 @@ class TypingPlayingScreen implements Screen {
     this.onStart();
   }
 
-  setWaiting(waiting: boolean): void {
-    this.gameContainer.classList.toggle('waiting', waiting);
+  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.play();
+      this.context.track.start(0);
     }
 
     this.setWaiting(false);
@@ -247,7 +255,7 @@ class TypingPlayingScreen implements Screen {
   }
 
   checkComplete(): void {
-    let currentLine = this.lines[this.currentIndex];
+    let currentLine = this.currentLine;
     if (
       currentLine != null &&
       currentLine.kana == '@' &&
@@ -276,7 +284,14 @@ class TypingPlayingScreen implements Screen {
       this.scoreController.intervalEnd(true);
     }
     if (this.context.track !== null) {
-      this.setWaiting(true);
+      // 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();
@@ -287,12 +302,18 @@ class TypingPlayingScreen implements Screen {
   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);
+      }
     }
   }
 
@@ -337,6 +358,7 @@ class TypingPlayingScreen implements Screen {
   exit(): void {}
 
   transitionExit(): void {
+    this.gameContainer.classList.remove('skippable');
     this.kanaController.destroy();
     this.romajiController.destroy();
     if (this.context.track !== null) {

+ 1 - 0
src/index.html

@@ -58,6 +58,7 @@
           <div class="kanji-line"></div>
           <div class="romaji-first"></div>
           <div class="romaji-line"></div>
+          <div class="skip-notice">Tab to skip</div>
           <div class="stats-line">
             <div class="pair">
               <span class="label">Hit</span>

+ 23 - 1
src/style.css

@@ -501,7 +501,7 @@
     '. . kana'
     '. . kanji'
     'romaji-first romaji romaji'
-    '. . stats';
+    'skip skip stats';
   grid-row-gap: 0.125em;
   padding: 1.25em 1.25em 0.125em 1.25em;
   background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
@@ -555,6 +555,16 @@
   animation: pulse 0.2s;
 }
 
+#game .skip-notice {
+  grid-area: skip;
+  opacity: 0;
+  font-size: 0.875em;
+}
+
+#game.skippable .skip-notice {
+  animation: 1.5s flash 0s 2;
+}
+
 /* }}} */
 
 /* score area {{{ */
@@ -669,6 +679,18 @@
   }
 }
 
+@keyframes flash {
+  0% {
+    opacity: 0;
+  }
+  50% {
+    opacity: 1;
+  }
+  100% {
+    opacity: 0;
+  }
+}
+
 #game.waiting .kana-line,
 #game.waiting .kanji-line {
   opacity: 0.5;