level.ts 4.9 KB

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