level.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. /**
  2. * This module represents the levels for the game. Each level consists of lines
  3. * that you have to complete. Each line has the kanji of the line, which is used
  4. * solely for display and the kana of the line which the input is based.
  5. */
  6. namespace level {
  7. export interface Line {
  8. kanji: string,
  9. kana: string,
  10. start?: number,
  11. end?: number
  12. }
  13. export interface Level {
  14. name: string,
  15. creator: string | null,
  16. genre: string | null,
  17. difficulty: string | null,
  18. audio: string | null,
  19. background?: string | null,
  20. lines: Line[]
  21. }
  22. export interface LevelSet {
  23. name: string,
  24. levels: Level[]
  25. }
  26. export interface Config {
  27. background: string,
  28. selectMusic: string | null,
  29. selectSound: string,
  30. decideSound: string,
  31. baseColor: string,
  32. highlightColor: string,
  33. levelSets: LevelSet[]
  34. }
  35. export function loadFromJson(url: string): Promise<Config> {
  36. return window.fetch(url)
  37. .then(response => response.json())
  38. }
  39. let parser = new DOMParser();
  40. function parseXML(response: Response): Promise<Document> {
  41. return response.text().then(text => {
  42. let normalized = text.replace(/[“”]/g, '"');
  43. return parser.parseFromString(normalized, "text/xml");
  44. });
  45. }
  46. export function loadFromTM(base: string): Promise<Config> {
  47. let settingsXML = window.fetch(base+'/settings.xml').then(parseXML);
  48. let levelSets = window.fetch(base+'/folderlist.xml')
  49. .then(parseXML)
  50. .then(dom => parseTMFolderList(base, dom));
  51. return Promise.all([settingsXML, levelSets]).then(pair => {
  52. return parseTMSettings(base, pair[1], pair[0]);
  53. })
  54. }
  55. function parseTMSettings(base: string, levelSets: LevelSet[], dom: Document): Config {
  56. function getData(tag: string): string | null {
  57. let elem = dom.querySelector(tag);
  58. if (elem === null) {
  59. return null;
  60. } else {
  61. return base+'/'+elem.getAttribute('src');
  62. }
  63. }
  64. let background = getData('background');
  65. let selectMusic = getData('selectmusic');
  66. let selectSound = getData('selectsound');
  67. let decideSound = getData('decidesound');
  68. if (background === null) {
  69. throw new Error('background is not set');
  70. }
  71. if (decideSound === null) {
  72. throw new Error('decidesound is not set');
  73. }
  74. if (selectSound === null) {
  75. throw new Error('selectsound is not set');
  76. }
  77. return {
  78. background,
  79. baseColor: 'white',
  80. highlightColor: 'blue',
  81. selectMusic,
  82. selectSound,
  83. decideSound,
  84. levelSets
  85. }
  86. }
  87. function parseTMFolderList(base: string, dom: Document): Promise<LevelSet[]> {
  88. let folderList = dom.querySelectorAll('folder');
  89. let promises = [];
  90. for (let i = 0; i < folderList.length; ++i) {
  91. let folder = folderList[i];
  92. let name = folder.getAttribute('name');
  93. let path = folder.getAttribute('path');
  94. if (name === null || path === null) {
  95. console.warn(`Invalid folder entry ${name} with path ${path}`);
  96. continue;
  97. }
  98. let promise = window.fetch(base+'/'+path)
  99. .then(parseXML)
  100. .then(dom => parseTMFolder(base, name!, dom))
  101. promises.push(promise);
  102. }
  103. return Promise.all(promises);
  104. }
  105. function parseTMFolder(base: string, name: string, dom: Document): Promise<LevelSet> {
  106. let musicList = dom.querySelectorAll('musicinfo');
  107. let promises = [];
  108. for (let i = 0; i < musicList.length; ++i) {
  109. let musicInfo = musicList[i];
  110. let xmlPath = base+'/'+musicInfo.getAttribute('xmlpath');
  111. let audioPath = base+'/'+musicInfo.getAttribute('musicpath');
  112. function getData(tag: string): string | null {
  113. let elem = musicInfo.querySelector(tag);
  114. if (elem === null) {
  115. return null;
  116. } else {
  117. return elem.textContent;
  118. }
  119. }
  120. let name = getData('musicname') || '[Unknown]';
  121. let creator = getData('artist');
  122. let genre = getData('genre');
  123. let difficulty = getData('level');
  124. let promise = window.fetch(xmlPath)
  125. .then(parseXML)
  126. .then(parseTMSong)
  127. .then(lines => {
  128. return {
  129. name,
  130. creator,
  131. genre,
  132. difficulty,
  133. audio: audioPath,
  134. lines
  135. }
  136. })
  137. promises.push(promise);
  138. }
  139. return Promise.all(promises)
  140. .then(levels => {
  141. return { name, levels }
  142. })
  143. }
  144. function parseTMSong(dom: Document): Line[] {
  145. let kanjiList = dom.querySelectorAll('nihongoword');
  146. let kanaList = dom.querySelectorAll('word');
  147. let intervalList = dom.querySelectorAll('interval');
  148. let lines: Line[] = [];
  149. let time = 0;
  150. for (let i = 0; i < intervalList.length; ++i) {
  151. let start = time;
  152. const interval = intervalList[i].textContent;
  153. if (interval === null) {
  154. throw new Error(`Invalid interval: ${interval}`);
  155. }
  156. time += parseInt(interval) / 1000
  157. lines.push({
  158. kanji: kanjiList[i].textContent || '',
  159. kana: kanaList[i].textContent || '',
  160. start: start,
  161. end: time
  162. })
  163. }
  164. return lines;
  165. }
  166. }