瀏覽代碼

Implement scoring

Thomas Dy 7 年之前
父節點
當前提交
bf4d7ea719
共有 4 個文件被更改,包括 203 次插入5 次删除
  1. 29 0
      dist/index.html
  2. 62 3
      dist/style.css
  3. 99 0
      src/display.ts
  4. 13 2
      src/game/typing.ts

+ 29 - 0
dist/index.html

@@ -25,10 +25,39 @@
           <template class="total" name="progress-bar"></template>
           <template class="interval" name="progress-bar"></template>
         </div>
+        <div class="score-line">
+          <span class="combo"></span>
+          <div class="pair">
+            <span class="label">Score</span>
+            <span class="value score"></span>
+          </div>
+          <div class="pair">
+            <span class="label">Max Combo</span>
+            <span class="value max-combo"></span>
+          </div>
+          <div class="pair">
+            <span class="label">Finished</span>
+            <span class="value finished"></span>
+          </div>
+        </div>
         <div class="kana-line"></div>
         <div class="kanji-line"></div>
         <div class="romaji-first"></div>
         <div class="romaji-line"></div>
+        <div class="stats-line">
+          <div class="pair">
+            <span class="label">Hit</span>
+            <span class="value hit"></span>
+          </div>
+          <div class="pair">
+            <span class="label">Missed</span>
+            <span class="value missed"></span>
+          </div>
+          <div class="pair">
+            <span class="label">Skipped</span>
+            <span class="value skipped"></span>
+          </div>
+        </div>
       </div>
       <div id="loader">
         <template name="progress-bar"></template>

+ 62 - 3
dist/style.css

@@ -410,19 +410,26 @@
 #game {
   display: grid;
   grid-template-columns: 50px 50px auto;
-  grid-template-rows: auto 12px 30px 40px;
+  grid-template-rows: 40px 24px 12px 30px auto 22px;
   grid-template-areas:
     ". . track"
+    "score score score"
     ". . kana"
     ". . kanji"
-    "romaji-first romaji romaji";
-  padding: 20px;
+    "romaji-first romaji romaji"
+    ". . stats";
+  grid-row-gap: 2px;
+  padding: 2px 20px;
 }
 
 #game .track-progress {
   grid-area: track;
 }
 
+#game .score-line {
+  grid-area: score;
+}
+
 #game .kana-line {
   grid-area: kana;
   font-size: 12px;
@@ -437,10 +444,15 @@
   border-radius: 0px 0px 0px 10px;
 }
 
+#game .stats-line {
+  grid-area: stats;
+}
+
 #game .romaji-first,
 #game .romaji-line {
   text-transform: uppercase;
   align-self: baseline;
+  line-height: 1;
 }
 
 #game .romaji-first {
@@ -459,6 +471,53 @@
   animation: pulse 0.2s;
 }
 
+
+/* }}} */
+
+/* score area {{{ */
+
+.score-line {
+  display: flex;
+}
+
+.score-line .pair,
+.stats-line .pair {
+  margin: 0px 4px;
+  border-bottom: solid 2px rgba(255, 255, 255, 0.5);
+  border-radius: 2px;
+}
+
+.score-line .pair span,
+.stats-line .pair span {
+  text-align: right;
+  padding: 0px 4px;
+}
+
+.score-line .combo {
+  flex: none;
+  width: 100px;
+  text-align: left;
+}
+
+.score-line .pair {
+  flex: 1;
+  display: flex;
+}
+
+.stats-line {
+  display: flex;
+  justify-content: right;
+}
+
+.stats-line .pair span {
+  font-size: 14px;
+}
+
+.stats-line .pair .value {
+  display: inline-block;
+  min-width: 50px;
+}
+
 /* }}} */
 
 .kana {

+ 99 - 0
src/display.ts

@@ -176,4 +176,103 @@ namespace display {
       this.totalBar.style.animationName = '';
     }
   }
+
+  export class ScoreController {
+    comboElement: HTMLElement;
+    scoreElement: HTMLElement;
+    maxComboElement: HTMLElement;
+    finishedElement: HTMLElement;
+    hitElement: HTMLElement;
+    missedElement: HTMLElement;
+    skippedElement: HTMLElement;
+
+    inputState: InputState | null;
+    observer: state.Observer;
+
+    combo: number = 0;
+    score: number = 0;
+    maxCombo: number = 0;
+    finished: number = 0;
+    hit: number = 0;
+    missed: number = 0;
+    skipped: number = 0;
+
+    constructor(
+      private scoreContainer: HTMLElement,
+      private statsContainer: HTMLElement
+    ) {
+      this.comboElement = scoreContainer.querySelector('.combo');
+      this.scoreElement = scoreContainer.querySelector('.score');
+      this.maxComboElement = scoreContainer.querySelector('.max-combo');
+      this.finishedElement = scoreContainer.querySelector('.finished');
+      this.hitElement = statsContainer.querySelector('.hit');
+      this.missedElement = statsContainer.querySelector('.missed');
+      this.skippedElement = statsContainer.querySelector('.skipped');
+      this.observer = result => this.update(result);
+      this.setValues();
+    }
+
+    setInputState(inputState: InputState): void {
+      this.clearObservers();
+      this.inputState = inputState;
+      if (this.inputState != null) {
+        this.inputState.map((_, m) => {
+          m.addObserver(this.observer);
+        });
+      }
+    }
+
+    intervalEnd(finished: boolean): void {
+      if (finished) {
+        this.finished += 1;
+      } else {
+        this.combo = 0;
+      }
+      this.setValues();
+    }
+
+    update(result: TransitionResult): void {
+      switch (result) {
+        case TransitionResult.SUCCESS:
+          this.hit += 1;
+          this.score += 100 + this.combo;
+          this.combo += 1;
+          break;
+        case TransitionResult.FAILED:
+          this.missed += 1;
+          this.combo = 0;
+          break;
+        case TransitionResult.SKIPPED:
+          this.skipped += 1;
+          this.combo = 0;
+          break;
+      }
+      if (this.combo > this.maxCombo) {
+        this.maxCombo = this.combo;
+      }
+      this.setValues();
+    }
+
+    setValues(): void {
+      this.comboElement.textContent = this.combo == 0 ? '' : this.combo+' combo';
+      this.scoreElement.textContent = this.score+'';
+      this.maxComboElement.textContent = this.maxCombo+'';
+      this.finishedElement.textContent = this.finished+'';
+      this.hitElement.textContent = this.hit+'';
+      this.missedElement.textContent = this.missed+'';
+      this.skippedElement.textContent = this.skipped+'';
+    }
+
+    private clearObservers(): void {
+      if (this.inputState != null) {
+        this.inputState.map((_, machine) => {
+          machine.removeObserver(this.observer);
+        });
+      }
+    }
+
+    destroy(): void {
+      this.clearObservers();
+    }
+  }
 }

+ 13 - 2
src/game/typing.ts

@@ -142,6 +142,7 @@ namespace game {
     kanaController: display.KanaDisplayController;
     romajiController: display.RomajiDisplayController;
     progressController: display.TrackProgressController | null;
+    scoreController: display.ScoreController;
     lines: level.Line[];
 
     constructor(readonly context: TypingScreenContext) {
@@ -157,6 +158,10 @@ namespace game {
         this.gameContainer.querySelector('.kana-line')
       );
       this.progressController = null;
+      this.scoreController = new display.ScoreController(
+        this.gameContainer.querySelector('.score-line'),
+        this.gameContainer.querySelector('.stats-line')
+      );
       this.lines = this.context.level.lines;
     }
 
@@ -195,7 +200,7 @@ namespace game {
     checkComplete(): void {
       let currentLine = this.lines[this.currentIndex];
       if (currentLine.kana == '@' && currentLine.kanji == '@') {
-        this.onComplete();
+        this.onComplete(true);
       }
     }
 
@@ -204,12 +209,16 @@ namespace game {
         this.setWaiting(false);
       } else {
         this.nextLine();
+        this.scoreController.intervalEnd(false);
       }
       this.checkComplete();
     }
 
-    onComplete(): void {
+    onComplete(autoComplete: boolean = false): void {
       this.nextLine();
+      if (!autoComplete) {
+        this.scoreController.intervalEnd(true);
+      }
       if (this.context.track !== null) {
         this.setWaiting(true);
       }
@@ -252,6 +261,7 @@ namespace game {
       this.kanjiElement.textContent = kanji;
       this.kanaController.setInputState(this.inputState);
       this.romajiController.setInputState(this.inputState);
+      this.scoreController.setInputState(this.inputState);
     }
 
     exit(): void {
@@ -266,6 +276,7 @@ namespace game {
         this.romajiController.destroy();
         this.progressController.destroy();
       }
+      this.scoreController.destroy();
     }
   }
 }