瀏覽代碼

Turn on strict and bruteforce the code to pass

Thomas Dy 6 年之前
父節點
當前提交
205f3b5b70
共有 11 個文件被更改,包括 200 次插入146 次删除
  1. 3 2
      src/audio.ts
  2. 21 17
      src/display.ts
  3. 43 44
      src/editor.ts
  4. 2 2
      src/game.ts
  5. 5 5
      src/game/common.ts
  6. 4 4
      src/game/loading.ts
  7. 23 18
      src/game/select.ts
  8. 41 36
      src/game/typing.ts
  9. 30 11
      src/level.ts
  10. 27 6
      src/util.ts
  11. 1 1
      tsconfig.json

+ 3 - 2
src/audio.ts

@@ -33,7 +33,7 @@ namespace audio {
         .then(audioBuffer => new Track(this, audioBuffer))
     }
 
-    loadTrackWithProgress(url: string, listener: EventListener): Promise<Track> {
+    loadTrackWithProgress(url: string, listener: (event: ProgressEvent) => any): Promise<Track> {
       let promise = new Promise<ArrayBuffer>((resolve, reject) => {
         let xhr = new XMLHttpRequest();
         xhr.open('GET', url);
@@ -61,6 +61,7 @@ namespace audio {
     constructor(manager: AudioManager, buffer: AudioBuffer) {
       this.manager = manager;
       this.buffer = buffer;
+      this.source = null;
       this.playStartTime = 0;
       this.resumeTime = 0;
       this.hasStarted = false;
@@ -77,7 +78,7 @@ namespace audio {
       this.source.start();
     }
 
-    start(duration: number = undefined): void {
+    start(duration?: number): void {
       this.source = this.manager.context.createBufferSource();
       this.source.buffer = this.buffer;
       this.source.connect(this.manager.output);

+ 21 - 17
src/display.ts

@@ -22,7 +22,6 @@ namespace display {
     element: HTMLElement;
     state: state.StateMachine;
     observer: state.Observer;
-    remove: () => void;
 
     constructor(kana: string, state: state.StateMachine) {
       this.state = state;
@@ -57,7 +56,7 @@ namespace display {
       this.children = [];
     }
 
-    setInputState(inputState: InputState) {
+    setInputState(inputState: InputState | null) {
       this.clearChildren();
       if (inputState == null) {
         this.children = [];
@@ -90,9 +89,10 @@ namespace display {
       readonly restElement: HTMLElement
     ) {
       this.observer = (result) => this.rerender(result);
+      this.inputState = null;
     }
 
-    setInputState(inputState: InputState) {
+    setInputState(inputState: InputState | null) {
       this.clearObservers();
       this.inputState = inputState;
       if (this.inputState != null) {
@@ -119,10 +119,13 @@ namespace display {
         this.firstElement.classList.remove('error');
         this.firstElement.offsetHeight; // trigger reflow
         this.firstElement.classList.add('error');
-      } else {
+      } else if (this.inputState !== null) {
         let remaining = this.inputState.getRemainingInput();
         this.firstElement.textContent = remaining.charAt(0);
         this.restElement.textContent = remaining.substring(1);
+      } else {
+        this.firstElement.textContent = '';
+        this.restElement.textContent = '';
       }
     }
 
@@ -134,11 +137,12 @@ namespace display {
   export class TrackProgressController {
     totalBar: HTMLElement;
     intervalBar: HTMLElement;
-    listener: (event: AnimationEvent) => void;
+    listener: ((event: AnimationEvent) => void) | null;
 
     constructor(private element: HTMLElement, lines: level.Line[]) {
-      this.totalBar = element.querySelector('.total .shade');
-      this.intervalBar = element.querySelector('.interval .shade');
+      this.totalBar = util.getElement(element, '.total .shade');
+      this.intervalBar = util.getElement(element, '.interval .shade');
+      this.listener = null;
 
       let totalDuration = lines[lines.length - 1].end;
       this.totalBar.style.animationName = 'progress';
@@ -146,7 +150,7 @@ namespace display {
 
       let names = lines.map(line => 'progress').join(',');
       let delays = lines.map(line => line.start + 's').join(',');
-      let durations = lines.map(line => (line.end - line.start) + 's').join(',');
+      let durations = lines.map(line => (line.end! - line.start!) + 's').join(',');
       this.intervalBar.style.animationName = names;
       this.intervalBar.style.animationDelay = delays;
       this.intervalBar.style.animationDuration = durations;
@@ -225,7 +229,7 @@ namespace display {
     missedElement: HTMLElement;
     skippedElement: HTMLElement;
 
-    inputState: InputState | null;
+    inputState: InputState | null = null;
     observer: state.Observer;
     score: Score;
 
@@ -234,19 +238,19 @@ namespace display {
       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.comboElement = util.getElement(scoreContainer, '.combo');
+      this.scoreElement = util.getElement(scoreContainer, '.score');
+      this.maxComboElement = util.getElement(scoreContainer, '.max-combo');
+      this.finishedElement = util.getElement(scoreContainer, '.finished');
+      this.hitElement = util.getElement(statsContainer, '.hit');
+      this.missedElement = util.getElement(statsContainer, '.missed');
+      this.skippedElement = util.getElement(statsContainer, '.skipped');
       this.observer = result => this.update(result);
       this.score = new Score();
       this.setValues();
     }
 
-    setInputState(inputState: InputState): void {
+    setInputState(inputState: InputState | null): void {
       this.clearObservers();
       this.inputState = inputState;
       if (this.inputState != null) {

+ 43 - 44
src/editor.ts

@@ -15,42 +15,41 @@ namespace editor {
     jsonElement: HTMLInputElement;
     track: audio.Track | null = null;
     markers: Marker[] = [];
-    rafId: number;
     waveForm: WaveForm;
-    currentMarker: Marker;
+    currentMarker: Marker | null = null;
 
     constructor() {
       this.audioManager = new audio.AudioManager();
-      this.audioElement = document.querySelector('#audio');
+      this.audioElement = util.getElement(document, '#audio');
       this.audioElement.addEventListener('change', event => {
         this.loadAudio();
       });
-      this.barElement = document.querySelector('.bar-overlay');
-      this.markerListElement = document.querySelector('.markers');
-      this.intervalListElement = document.querySelector('#intervals');
-      this.kanaElement = document.querySelector('#kana');
-      this.kanjiElement = document.querySelector('#kanji');
-      this.displayElement = document.querySelector('#display');
-      this.jsonElement = document.querySelector('#json');
+      this.barElement = util.getElement(document, '.bar-overlay');
+      this.markerListElement = util.getElement(document, '.markers');
+      this.intervalListElement = util.getElement(document, '#intervals');
+      this.kanaElement = util.getElement(document, '#kana');
+      this.kanjiElement = util.getElement(document, '#kanji');
+      this.displayElement = util.getElement(document, '#display');
+      this.jsonElement = util.getElement(document, '#json');
       this.waveForm = new WaveForm(
-        document.querySelector('#waveform'),
-        document.querySelector('#waveform-overlay'),
+        util.getElement(document, '#waveform'),
+        util.getElement(document, '#waveform-overlay'),
         (time: number) => this.play(time)
       );
 
       this.markerListElement.addEventListener('click', (event: MouseEvent) => this.markersClick(event));
-      document.querySelector('#play').addEventListener('click', () => this.play());
-      document.querySelector('#pause').addEventListener('click', () => this.pause());
-      document.querySelector('#insert-marker').addEventListener('click', () => this.insertMarker());
-      document.querySelector('.bar').addEventListener('click', (event: MouseEvent) => this.scrubberClick(event));
-      document.querySelector('#import').addEventListener('click', () => this.import());
-      document.querySelector('#export').addEventListener('click', () => this.export());
+      document.querySelector('#play')!.addEventListener('click', () => this.play());
+      document.querySelector('#pause')!.addEventListener('click', () => this.pause());
+      document.querySelector('#insert-marker')!.addEventListener('click', () => this.insertMarker());
+      document.querySelector<HTMLElement>('.bar')!.addEventListener('click', (event: MouseEvent) => this.scrubberClick(event));
+      document.querySelector('#import')!.addEventListener('click', () => this.import());
+      document.querySelector('#export')!.addEventListener('click', () => this.export());
 
       this.update();
     }
 
     loadAudio(): void {
-      let file = this.audioElement.files[0];
+      let file = this.audioElement.files![0];
       if (file != null) {
         if (this.track != null) {
           this.track.stop();
@@ -71,7 +70,7 @@ namespace editor {
         if (this.currentMarker) {
           this.currentMarker.liElement.className = '';
         }
-        let index = this.markers.findIndex(m => m.time > this.track.getTime());
+        let index = this.markers.findIndex(m => m.time > this.track!.getTime());
         if (index < 0) index = 0;
         this.currentMarker = this.markers[index - 1];
         if (this.currentMarker) {
@@ -87,27 +86,27 @@ namespace editor {
       let pos = event.clientX - 10;
       console.log(pos);
       let percentage = pos / this.markerListElement.clientWidth;
-      let targetTime = percentage * this.track.getDuration();
+      let targetTime = percentage * this.track!.getDuration();
       this.play(targetTime);
     }
 
     markersClick(event: MouseEvent): void {
       let pos = event.clientX - 10;
       let percentage = pos / this.markerListElement.clientWidth;
-      let targetTime = percentage * this.track.getDuration();
+      let targetTime = percentage * this.track!.getDuration();
       this.insertMarker(targetTime);
     }
 
-    insertMarker(time: number = undefined): void {
+    insertMarker(time?: number): void {
       let marker = new Marker(
-        this.track.getDuration(),
+        this.track!.getDuration(),
         (marker: Marker) => this.removeMarker(marker),
         (marker: Marker) => this.playMarker(marker)
       );
       if (time !== undefined) {
         marker.time = time;
       } else {
-        marker.time = this.track.getTime();
+        marker.time = this.track!.getTime();
       }
       let insertIndex = this.markers.findIndex(m => m.time > marker.time);
       if (insertIndex >= 0) {
@@ -123,21 +122,21 @@ namespace editor {
       }
     }
 
-    play(start: number = undefined, duration: number = undefined): void {
-      this.track.pause();
+    play(start?: number, duration?: number): void {
+      this.track!.pause();
       if (start != undefined) {
-        this.track.resumeTime = start;
+        this.track!.resumeTime = start;
       }
-      this.track.start(duration);
+      this.track!.start(duration);
     }
 
     pause(): void {
-      this.track.pause();
+      this.track!.pause();
     }
 
     playMarker(marker: Marker): void {
       let start = marker.time;
-      let end = this.track.getDuration();
+      let end = this.track!.getDuration();
       let index = this.markers.findIndex(m => m == marker);
       if (index < this.markers.length - 1) {
         end = this.markers[index + 1].time;
@@ -234,14 +233,14 @@ namespace editor {
       this.markerElement.className = 'marker';
 
       let fragment = util.loadTemplate('interval');
-      this.liElement = fragment.querySelector('*');
-      this.inputElement = fragment.querySelector('.interval');
+      this.liElement = util.getElement(fragment, '*');
+      this.inputElement = util.getElement(fragment, '.interval');
       this.inputElement.addEventListener('change', () => {
         this.time = parseFloat(this.inputElement.value);
       });
 
-      fragment.querySelector('.play-section').addEventListener('click', () => play(this));
-      fragment.querySelector('.remove-section').addEventListener('click', () => remove(this));
+      fragment.querySelector('.play-section')!.addEventListener('click', () => play(this));
+      fragment.querySelector('.remove-section')!.addEventListener('click', () => remove(this));
     }
 
     get time(): number {
@@ -258,10 +257,10 @@ namespace editor {
   class WaveForm {
     ctx: CanvasRenderingContext2D;
     overlayCtx: CanvasRenderingContext2D;
-    track: audio.Track | null;
-    data: Float32Array | null;
-    stride: number;
-    currentSection: number;
+    track: audio.Track | null = null;
+    data: Float32Array | null = null;
+    stride: number = 0;
+    currentSection: number = -1;
 
     constructor(
       readonly canvas: HTMLCanvasElement,
@@ -272,8 +271,8 @@ namespace editor {
       canvas.width = canvas.clientWidth;
       overlay.height = overlay.clientHeight;
       overlay.width = overlay.clientWidth;
-      this.ctx = canvas.getContext('2d');
-      this.overlayCtx = overlay.getContext('2d');
+      this.ctx = canvas.getContext('2d')!;
+      this.overlayCtx = overlay.getContext('2d')!;
 
       this.overlayCtx.fillStyle = 'rgba(255, 0, 0, 0.5)';
 
@@ -296,11 +295,11 @@ namespace editor {
     }
 
     update(markers: Marker[]): void {
-      let section = Math.floor(this.track.getTime() / 5);
+      let section = Math.floor(this.track!.getTime() / 5);
 
       let height = this.canvas.height;
       if (this.currentSection != section) {
-        this.data = this.track.buffer.getChannelData(0);
+        this.data = this.track!.buffer.getChannelData(0);
         this.ctx.clearRect(0, 0, this.canvas.width, height);
         this.ctx.beginPath();
         this.ctx.moveTo(0, height / 2);
@@ -314,7 +313,7 @@ namespace editor {
         this.currentSection = section;
       }
 
-      let marker = this.timeToX(this.track.getTime());
+      let marker = this.timeToX(this.track!.getTime());
       this.overlayCtx.clearRect(0, 0, this.canvas.width, height);
       this.overlayCtx.fillRect(0, 0, marker, height);
       markers.forEach(m => {

+ 2 - 2
src/game.ts

@@ -11,7 +11,7 @@ namespace game {
       super(container);
 
       let self = this;
-      let bgLayer: HTMLElement = container.querySelector('#background');
+      let bgLayer: HTMLElement = util.getElement(container, '#background');
       let gameContext: GameContext = {
         container: container,
         audioManager: new audio.AudioManager(),
@@ -26,7 +26,7 @@ namespace game {
       this.loadingScreen = new LoadingScreen(gameContext, configUrl);
 
       document.addEventListener('keydown', (event) => {
-        if (!event.ctrlKey && !event.metaKey) {
+        if (this.activeScreen !== null && !event.ctrlKey && !event.metaKey) {
           this.activeScreen.handleInput(event.key);
         }
       });

+ 5 - 5
src/game/common.ts

@@ -27,7 +27,7 @@ namespace game {
       });
     }
 
-    switchScreen(nextScreen: Screen): void {
+    switchScreen(nextScreen: Screen | null): void {
       if (this.activeScreen != null) {
         this.container.classList.remove(this.activeScreen.name);
         this.pendingExit = true;
@@ -36,15 +36,15 @@ namespace game {
       }
       this.activeScreen = nextScreen;
       if (nextScreen != null) {
-        this.activeScreen.enter();
-        this.container.classList.add(this.activeScreen.name);
+        nextScreen.enter();
+        this.container.classList.add(nextScreen.name);
       }
     }
   }
 
   interface GameSounds {
-    selectSound: audio.Track,
-    decideSound: audio.Track
+    selectSound: audio.Track | null,
+    decideSound: audio.Track | null
   }
 
   export interface GameContext {

+ 4 - 4
src/game/loading.ts

@@ -22,7 +22,7 @@ namespace game {
     }
 
     loadAssets(): void {
-      let config = this.context.config;
+      let config = this.context.config!;
 
       Promise.all([
         this.loadImage(config.background),
@@ -40,7 +40,7 @@ namespace game {
     }
 
     finishLoading(): void {
-      let loadingElement: HTMLElement = this.context.container.querySelector('#loading');
+      let loadingElement: HTMLElement = util.getElement(this.context.container, '#loading');
       loadingElement.addEventListener('transitionend', (event) => {
         loadingElement.style.display = 'none';
         this.switchToSelect()
@@ -48,7 +48,7 @@ namespace game {
       loadingElement.classList.add('finished');
     }
 
-    loadTrack(url: string): Promise<audio.Track> {
+    loadTrack(url: string): Promise<audio.Track | null> {
       if (url == null) {
         return Promise.resolve(null);
       } else {
@@ -76,7 +76,7 @@ namespace game {
     handleInput(key: string): void {}
 
     exit(): void {
-      let config = this.context.config;
+      let config = this.context.config!;
       let containerStyle = this.context.container.style;
       containerStyle.setProperty('--base-color', config.baseColor);
       containerStyle.setProperty('--highlight-color', config.highlightColor);

+ 23 - 18
src/game/select.ts

@@ -14,7 +14,7 @@ namespace game {
     init: boolean;
 
     get levelSets() {
-      return this.context.config.levelSets;
+      return this.context.config!.levelSets;
     }
 
     get currentLevelSet() {
@@ -27,9 +27,10 @@ namespace game {
 
     constructor(private context: GameContext) {
       let container = context.container;
-      this.folderInfo = container.querySelector('#folder-info');
-      this.songInfo = container.querySelector('#song-info');
-      this.songList = container.querySelector('#song-list');
+      this.folderInfo = util.getElement(container, '#folder-info');
+      this.currentFolderIndex = 0;
+      this.songInfo = util.getElement(container, '#song-info');
+      this.songList = util.getElement(container, '#song-list');
 
       this.listControllers = [];
       this.levelSets.forEach(levelSet => {
@@ -53,7 +54,7 @@ namespace game {
     }
 
     enter(): void {
-      this.context.bgManager.setBackground(this.context.config.background);
+      this.context.bgManager.setBackground(this.context.config!.background);
       this.folderController.listeners.attach();
     }
 
@@ -63,8 +64,9 @@ namespace game {
     }
 
     selectSong(index: number): void {
-      if (!this.init) {
-        this.context.assets.selectSound.play();
+      const { selectSound } = this.context.assets!;
+      if (!this.init && selectSound !== null) {
+        selectSound.play();
       }
       let song = this.currentLevelSet.levels[index];
       let songInfoComponent = new SongInfoComponent(song);
@@ -73,7 +75,10 @@ namespace game {
     }
 
     chooseSong(index: number): void {
-      this.context.assets.decideSound.play();
+      const { decideSound } = this.context.assets!;
+      if (decideSound !== null) {
+        decideSound.play();
+      }
       let level = this.currentLevelSet.levels[index];
       let gameScreen = new game.TypingScreen(this.context, level, this);
       this.context.switchScreen(gameScreen);
@@ -101,18 +106,18 @@ namespace game {
     listeners: util.ListenersManager;
 
     constructor(element: HTMLElement, levelSets: level.LevelSet[], onFolderChange: (index: number) => void) {
-      this.labelElement = element.querySelector('.label');
+      this.labelElement = util.getElement(element, '.label');
       this.levelSets = levelSets;
       this.currentIndex = 0;
       this.onFolderChange = onFolderChange;
       this.listeners = new util.ListenersManager();
       this.listeners.add(
-        element.querySelector('.left'),
+        element.querySelector('.left')!,
         'click',
         () => this.scroll(-1)
       );
       this.listeners.add(
-        element.querySelector('.right'),
+        element.querySelector('.right')!,
         'click',
         () => this.scroll(1)
       );
@@ -144,9 +149,9 @@ namespace game {
 
     constructor(level: level.Level) {
       this.element = util.loadTemplate('song-info');
-      this.element.querySelector('.genre').textContent = level.genre;
-      this.element.querySelector('.creator').textContent = level.creator;
-      this.element.querySelector('.title').textContent = level.name;
+      this.element.querySelector('.genre')!.textContent = level.genre;
+      this.element.querySelector('.creator')!.textContent = level.creator;
+      this.element.querySelector('.title')!.textContent = level.name;
     }
   }
 
@@ -173,10 +178,10 @@ namespace game {
 
       this.levels.forEach((level, index) => {
         let element = util.loadTemplate('song-item');
-        element.querySelector('.creator').textContent = level.creator;
-        element.querySelector('.title').textContent = level.name;
-        element.querySelector('.difficulty').textContent = level.difficulty;
-        element.querySelector('.song-item').addEventListener('click', (event) => this.click(index));
+        element.querySelector('.creator')!.textContent = level.creator;
+        element.querySelector('.title')!.textContent = level.name;
+        element.querySelector('.difficulty')!.textContent = level.difficulty;
+        element.querySelector('.song-item')!.addEventListener('click', (event) => this.click(index));
         this.element.appendChild(element);
       });
       this.element.children[0].classList.add('selected');

+ 41 - 36
src/game/typing.ts

@@ -13,7 +13,7 @@ namespace game {
     constructor(
       readonly context: GameContext,
       readonly level: Level,
-      readonly switchClosure: (screen: Screen) => void
+      readonly switchClosure: (screen: Screen | null) => void
     ) {}
 
     get container() {
@@ -28,7 +28,7 @@ namespace game {
       return this.context.bgManager;
     }
 
-    switchScreen(screen: Screen): void {
+    switchScreen(screen: Screen | null): void {
       this.switchClosure(screen);
     }
   }
@@ -54,10 +54,12 @@ namespace game {
     }
 
     handleInput(key: string): void {
-      this.activeScreen.handleInput(key);
+      if (this.activeScreen !== null) {
+        this.activeScreen.handleInput(key);
+      }
     }
 
-    switchScreen(screen: Screen): void {
+    switchScreen(screen: Screen | null): void {
       super.switchScreen(screen);
       if (screen == null) {
         this.context.switchScreen(this.prevScreen);
@@ -71,25 +73,25 @@ namespace game {
 
   class TypingLoadingScreen implements Screen {
     readonly name: string = 'game-loading';
-    barElement: HTMLElement;
-    textElement: HTMLElement;
-    readyElement: HTMLElement;
+    barElement: HTMLElement | null = null;
+    textElement: HTMLElement | null = null;
+    readyElement: HTMLElement | null = null;
     isReady: boolean = false;
 
     constructor(readonly context: TypingScreenContext) {}
 
     enter(): void {
-      let loader: HTMLElement = this.context.container.querySelector('#loader');
-      this.readyElement = this.context.container.querySelector('#ready');
+      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 = loader.querySelector('.progress-bar .shade');
-        this.textElement = loader.querySelector('.label');
+        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.readyElement.querySelector('.status')!.textContent = 'Loading';
+        this.readyElement.querySelector('.message')!.textContent = 'please wait';
 
         this.context.audioManager.loadTrackWithProgress(
           this.context.level.audio,
@@ -97,13 +99,13 @@ namespace game {
             if (event.lengthComputable) {
               // only up to 80 to factor in decoding time
               let percentage = event.loaded / event.total * 80;
-              this.barElement.style.width = `${percentage}%`;
+              this.barElement!.style.width = `${percentage}%`;
             }
           }
         ).then(track => {
           this.context.track = track;
-          this.barElement.style.width = '100%';
-          this.textElement.textContent = 'music loaded';
+          this.barElement!.style.width = '100%';
+          this.textElement!.textContent = 'music loaded';
           this.setReady();
         });
 
@@ -114,8 +116,8 @@ namespace game {
     }
 
     setReady(): void {
-      this.readyElement.querySelector('.status').textContent = 'Ready';
-      this.readyElement.querySelector('.message').textContent = 'press space to start';
+      this.readyElement!.querySelector('.status')!.textContent = 'Ready';
+      this.readyElement!.querySelector('.message')!.textContent = 'press space to start';
       this.isReady = true;
     }
 
@@ -130,7 +132,9 @@ namespace game {
     exit(): void {}
 
     transitionExit(): void {
-      this.barElement.style.width = '0%';
+      if (this.barElement) {
+        this.barElement.style.width = '0%';
+      }
     }
   }
 
@@ -148,27 +152,28 @@ namespace game {
     lines: level.Line[];
 
     constructor(readonly context: TypingScreenContext) {
-      this.gameContainer = this.context.container.querySelector('#game');
+      this.gameContainer = util.getElement(this.context.container, '#game');
       this.currentIndex = -1;
       this.inputState = null;
-      this.kanjiElement = this.gameContainer.querySelector('.kanji-line');
+      this.isWaiting = false;
+      this.kanjiElement = util.getElement(this.gameContainer, '.kanji-line');
       this.romajiController = new display.RomajiDisplayController(
-        this.gameContainer.querySelector('.romaji-first'),
-        this.gameContainer.querySelector('.romaji-line')
+        util.getElement(this.gameContainer, '.romaji-first'),
+        util.getElement(this.gameContainer, '.romaji-line')
       );
       this.kanaController = new display.KanaDisplayController(
-        this.gameContainer.querySelector('.kana-line')
+        util.getElement(this.gameContainer, '.kana-line')
       );
       this.progressController = null;
       this.scoreController = new display.ScoreController(
-        this.gameContainer.querySelector('.score-line'),
-        this.gameContainer.querySelector('.stats-line')
+        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('.track-progress');
+      let progressElement: HTMLElement = this.gameContainer.querySelector<HTMLElement>('.track-progress')!;
       if (this.context.level.audio == null) {
         progressElement.style.visibility = 'hidden';
         this.lines = this.context.level.lines.filter(line => line.kana != "@");
@@ -191,7 +196,7 @@ namespace game {
     onStart(): void {
       this.nextLine();
       if (this.context.track !== null) {
-        this.progressController.start();
+        this.progressController!.start();
         this.context.track.play();
       }
 
@@ -286,7 +291,7 @@ namespace game {
       if (this.context.track !== null) {
         this.kanaController.destroy();
         this.romajiController.destroy();
-        this.progressController.destroy();
+        this.progressController!.destroy();
       }
       this.scoreController.destroy();
     }
@@ -299,13 +304,13 @@ namespace game {
       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+'';
+      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 {}

+ 30 - 11
src/level.ts

@@ -13,11 +13,11 @@ namespace level {
 
   export interface Level {
     name: string,
-    creator?: string,
-    genre?: string,
-    difficulty?: string,
-    audio?: string,
-    background?: string,
+    creator: string | null,
+    genre: string | null,
+    difficulty: string | null,
+    audio: string | null,
+    background?: string | null,
     lines: Line[]
   }
 
@@ -28,7 +28,7 @@ namespace level {
 
   export interface Config {
     background: string,
-    selectMusic?: string,
+    selectMusic: string | null,
     selectSound: string,
     decideSound: string,
     baseColor: string,
@@ -76,6 +76,16 @@ namespace level {
     let selectSound = getData('selectsound');
     let decideSound = getData('decidesound');
 
+    if (background === null) {
+      throw new Error('background is not set');
+    }
+    if (decideSound === null) {
+      throw new Error('decidesound is not set');
+    }
+    if (selectSound === null) {
+      throw new Error('selectsound is not set');
+    }
+
     return {
       background,
       baseColor: 'white',
@@ -95,9 +105,14 @@ namespace level {
       let name = folder.getAttribute('name');
       let path = folder.getAttribute('path');
 
+      if (name === null || path === null) {
+        console.warn(`Invalid folder entry ${name} with path ${path}`);
+        continue;
+      }
+
       let promise = window.fetch(base+'/'+path)
         .then(parseXML)
-        .then(dom => parseTMFolder(base, name, dom))
+        .then(dom => parseTMFolder(base, name!, dom))
 
       promises.push(promise);
     }
@@ -121,7 +136,7 @@ namespace level {
         }
       }
 
-      let name = getData('musicname');
+      let name = getData('musicname') || '[Unknown]';
       let creator = getData('artist');
       let genre = getData('genre');
       let difficulty = getData('level');
@@ -157,11 +172,15 @@ namespace level {
     let time = 0;
     for (let i = 0; i < intervalList.length; ++i) {
       let start = time;
-      time += parseInt(intervalList[i].textContent) / 1000
+      const interval = intervalList[i].textContent;
+      if (interval === null) {
+        throw new Error(`Invalid interval: ${interval}`);
+      }
+      time += parseInt(interval) / 1000
 
       lines.push({
-        kanji: kanjiList[i].textContent,
-        kana: kanaList[i].textContent,
+        kanji: kanjiList[i].textContent || '',
+        kana: kanaList[i].textContent || '',
         start: start,
         end: time
       })

+ 27 - 6
src/util.ts

@@ -1,14 +1,23 @@
 namespace util {
   export function loadBase(): void {
     let container = document.querySelector('#container');
+    if (container === null) {
+      throw new Error('Container not found');
+    }
 
     let baseTemplate = loadTemplate('base');
     baseTemplate.querySelectorAll('template').forEach(t => {
-      let parent = t.parentNode;
-      let template = loadTemplate(t.getAttribute('name'));
+      let parent = t.parentNode as Node;
+      const templateName = t.getAttribute('name');
+      if (templateName === null) {
+        return;
+      }
+      let template = loadTemplate(templateName);
       let firstElement = template.querySelector('*');
-      for (let i = 0; i < t.classList.length; ++i) {
-        firstElement.classList.add(t.classList[i]);
+      if (firstElement !== null) {
+        for (let i = 0; i < t.classList.length; ++i) {
+          firstElement.classList.add(t.classList[i]);
+        }
       }
       parent.insertBefore(template, t);
       parent.removeChild(t);
@@ -18,8 +27,12 @@ namespace util {
   }
 
   export function loadTemplate(id: string): DocumentFragment {
-    let template: HTMLTemplateElement = document.querySelector(`#${id}-template`);
-    return document.importNode(template.content, true);
+    let template = document.querySelector(`#${id}-template`);
+    if (template !== null && template instanceof HTMLTemplateElement) {
+      return document.importNode(template.content, true);
+    } else {
+      throw new Error(`#${id}-template is not a template`);
+    }
   }
 
   export function clearChildren(node: Node): void {
@@ -28,6 +41,14 @@ namespace util {
     }
   }
 
+  export function getElement<E extends HTMLElement>(element: ParentNode, selector: string): E {
+    const e = element.querySelector(selector);
+    if (e === null) {
+      throw new Error(`Could not find required element ${selector}`);
+    }
+    return e as E;
+  }
+
   class ListenerManager {
     constructor(
       private target: EventTarget,

+ 1 - 1
tsconfig.json

@@ -1,6 +1,6 @@
 {
   "compilerOptions": {
-    "noImplicitAny": true,
+    "strict": true,
     "removeComments": true,
     "sourceMap": true,
     "outFile": "dist/bundle.js",