level.ts 5.1 KB

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