| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187 | /** * This module represents the levels for the game. Each level consists of lines * that you have to complete. Each line has the kanji of the line, which is used * solely for display and the kana of the line which the input is based. */namespace level {  export interface Line {    kanji: string,    kana: string,    start?: number,    end?: number  }  export interface Level {    name: string,    creator: string | null,    genre: string | null,    difficulty: string | null,    audio: string | null,    background?: string | null,    songLink?: string,    lines: Line[]  }  export interface LevelSet {    name: string,    levels: Level[]  }  export interface Config {    background: string,    selectMusic: string | null,    selectSound: string,    decideSound: string,    baseColor: string,    highlightColor: string,    levelSets: LevelSet[]  }  export async function loadFromJson(url: string): Promise<Config> {    const response = await window.fetch(url);    return await response.json();  }  let parser = new DOMParser();  async function parseXML(response: Response): Promise<Document> {    const text = await response.text();    let normalized = text.replace(/[“”]/g, '"');    return parser.parseFromString(normalized, "text/xml");  }  export async function loadFromTM(base: string): Promise<Config> {    let settingsXML = window.fetch(base+'/settings.xml').then(parseXML);    let levelSets = window.fetch(base+'/folderlist.xml')      .then(parseXML)      .then(dom => parseTMFolderList(base, dom));    const [settings, levels] = await Promise.all([settingsXML, levelSets]);    return parseTMSettings(base, levels, settings);  }  function parseTMSettings(base: string, levelSets: LevelSet[], dom: Document): Config {    function getData(tag: string): string | null {      let elem = dom.querySelector(tag);      if (elem === null) {        return null;      } else {        return base+'/'+elem.getAttribute('src');      }    }    let background = getData('background');    let selectMusic = getData('selectmusic');    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',      highlightColor: 'blue',      selectMusic,      selectSound,      decideSound,      levelSets    }  }  function parseTMFolderList(base: string, dom: Document): Promise<LevelSet[]> {    let folderList = dom.querySelectorAll('folder');    let promises = [];    for (let i = 0; i < folderList.length; ++i) {      let folder = folderList[i];      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))      promises.push(promise);    }    return Promise.all(promises);  }  async function parseTMFolder(base: string, name: string, dom: Document): Promise<LevelSet> {    let musicList = dom.querySelectorAll('musicinfo');    let promises = [];    for (let i = 0; i < musicList.length; ++i) {      let musicInfo = musicList[i];      let xmlPath = base+'/'+musicInfo.getAttribute('xmlpath');      let audioPath = base+'/'+musicInfo.getAttribute('musicpath');      function getData(tag: string): string | null {        let elem = musicInfo.querySelector(tag);        if (elem === null) {          return null;        } else {          return elem.textContent;        }      }      let name = getData('musicname') || '[Unknown]';      let creator = getData('artist');      let genre = getData('genre');      let difficulty = getData('level');      let promise = window.fetch(xmlPath)        .then(parseXML)        .then(parseTMSong)        .then(lines => {          return {            name,            creator,            genre,            difficulty,            audio: audioPath,            lines          }        })      promises.push(promise);    }    const levels = await Promise.all(promises);    return { name, levels }  }  function parseTMSong(dom: Document): Line[] {    let kanjiList = dom.querySelectorAll('nihongoword');    let kanaList = dom.querySelectorAll('word');    let intervalList = dom.querySelectorAll('interval');    let lines: Line[] = [];    let time = 0;    for (let i = 0; i < intervalList.length; ++i) {      let start = time;      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 || '',        start: start,        end: time      })    }    return lines;  }}
 |