idlerpg.sh 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (478) hide show
  1. package/README.md +171 -0
  2. package/dist/affix/AffixRegistry.d.ts +68 -0
  3. package/dist/affix/AffixRegistry.d.ts.map +1 -0
  4. package/dist/affix/AffixRegistry.js +245 -0
  5. package/dist/affix/AffixRegistry.js.map +1 -0
  6. package/dist/affix/PluginLoader.d.ts +66 -0
  7. package/dist/affix/PluginLoader.d.ts.map +1 -0
  8. package/dist/affix/PluginLoader.js +321 -0
  9. package/dist/affix/PluginLoader.js.map +1 -0
  10. package/dist/affix/builtin.d.ts +6 -0
  11. package/dist/affix/builtin.d.ts.map +1 -0
  12. package/dist/affix/builtin.js +311 -0
  13. package/dist/affix/builtin.js.map +1 -0
  14. package/dist/affix/index.d.ts +8 -0
  15. package/dist/affix/index.d.ts.map +1 -0
  16. package/dist/affix/index.js +11 -0
  17. package/dist/affix/index.js.map +1 -0
  18. package/dist/affix/types.d.ts +152 -0
  19. package/dist/affix/types.d.ts.map +1 -0
  20. package/dist/affix/types.js +8 -0
  21. package/dist/affix/types.js.map +1 -0
  22. package/dist/ai/PlayKitClient.d.ts +208 -0
  23. package/dist/ai/PlayKitClient.d.ts.map +1 -0
  24. package/dist/ai/PlayKitClient.js +721 -0
  25. package/dist/ai/PlayKitClient.js.map +1 -0
  26. package/dist/ai/index.d.ts +5 -0
  27. package/dist/ai/index.d.ts.map +1 -0
  28. package/dist/ai/index.js +8 -0
  29. package/dist/ai/index.js.map +1 -0
  30. package/dist/app/App.d.ts +85 -0
  31. package/dist/app/App.d.ts.map +1 -0
  32. package/dist/app/App.js +296 -0
  33. package/dist/app/App.js.map +1 -0
  34. package/dist/app/EventBus.d.ts +222 -0
  35. package/dist/app/EventBus.d.ts.map +1 -0
  36. package/dist/app/EventBus.js +88 -0
  37. package/dist/app/EventBus.js.map +1 -0
  38. package/dist/app/Keybind.d.ts +83 -0
  39. package/dist/app/Keybind.d.ts.map +1 -0
  40. package/dist/app/Keybind.js +184 -0
  41. package/dist/app/Keybind.js.map +1 -0
  42. package/dist/app/Router.d.ts +123 -0
  43. package/dist/app/Router.d.ts.map +1 -0
  44. package/dist/app/Router.js +142 -0
  45. package/dist/app/Router.js.map +1 -0
  46. package/dist/app/ScreenManager.d.ts +97 -0
  47. package/dist/app/ScreenManager.d.ts.map +1 -0
  48. package/dist/app/ScreenManager.js +216 -0
  49. package/dist/app/ScreenManager.js.map +1 -0
  50. package/dist/app/index.d.ts +14 -0
  51. package/dist/app/index.d.ts.map +1 -0
  52. package/dist/app/index.js +19 -0
  53. package/dist/app/index.js.map +1 -0
  54. package/dist/app/screens/BaseScreen.d.ts +101 -0
  55. package/dist/app/screens/BaseScreen.d.ts.map +1 -0
  56. package/dist/app/screens/BaseScreen.js +132 -0
  57. package/dist/app/screens/BaseScreen.js.map +1 -0
  58. package/dist/app/screens/CharacterCreationScreen.d.ts +42 -0
  59. package/dist/app/screens/CharacterCreationScreen.d.ts.map +1 -0
  60. package/dist/app/screens/CharacterCreationScreen.js +467 -0
  61. package/dist/app/screens/CharacterCreationScreen.js.map +1 -0
  62. package/dist/app/screens/CombatScreen.d.ts +30 -0
  63. package/dist/app/screens/CombatScreen.d.ts.map +1 -0
  64. package/dist/app/screens/CombatScreen.js +309 -0
  65. package/dist/app/screens/CombatScreen.js.map +1 -0
  66. package/dist/app/screens/DialogScreen.d.ts +29 -0
  67. package/dist/app/screens/DialogScreen.d.ts.map +1 -0
  68. package/dist/app/screens/DialogScreen.js +295 -0
  69. package/dist/app/screens/DialogScreen.js.map +1 -0
  70. package/dist/app/screens/ExploreScreen.d.ts +50 -0
  71. package/dist/app/screens/ExploreScreen.d.ts.map +1 -0
  72. package/dist/app/screens/ExploreScreen.js +308 -0
  73. package/dist/app/screens/ExploreScreen.js.map +1 -0
  74. package/dist/app/screens/HelpScreen.d.ts +12 -0
  75. package/dist/app/screens/HelpScreen.d.ts.map +1 -0
  76. package/dist/app/screens/HelpScreen.js +155 -0
  77. package/dist/app/screens/HelpScreen.js.map +1 -0
  78. package/dist/app/screens/InventoryScreen.d.ts +27 -0
  79. package/dist/app/screens/InventoryScreen.d.ts.map +1 -0
  80. package/dist/app/screens/InventoryScreen.js +326 -0
  81. package/dist/app/screens/InventoryScreen.js.map +1 -0
  82. package/dist/app/screens/PrologueScreen.d.ts +24 -0
  83. package/dist/app/screens/PrologueScreen.d.ts.map +1 -0
  84. package/dist/app/screens/PrologueScreen.js +176 -0
  85. package/dist/app/screens/PrologueScreen.js.map +1 -0
  86. package/dist/app/screens/TitleScreen.d.ts +42 -0
  87. package/dist/app/screens/TitleScreen.d.ts.map +1 -0
  88. package/dist/app/screens/TitleScreen.js +380 -0
  89. package/dist/app/screens/TitleScreen.js.map +1 -0
  90. package/dist/app/screens/TravelScreen.d.ts +22 -0
  91. package/dist/app/screens/TravelScreen.d.ts.map +1 -0
  92. package/dist/app/screens/TravelScreen.js +122 -0
  93. package/dist/app/screens/TravelScreen.js.map +1 -0
  94. package/dist/app/screens/index.d.ts +14 -0
  95. package/dist/app/screens/index.d.ts.map +1 -0
  96. package/dist/app/screens/index.js +17 -0
  97. package/dist/app/screens/index.js.map +1 -0
  98. package/dist/commands/CommandRegistry.d.ts +91 -0
  99. package/dist/commands/CommandRegistry.d.ts.map +1 -0
  100. package/dist/commands/CommandRegistry.js +159 -0
  101. package/dist/commands/CommandRegistry.js.map +1 -0
  102. package/dist/commands/index.d.ts +7 -0
  103. package/dist/commands/index.d.ts.map +1 -0
  104. package/dist/commands/index.js +10 -0
  105. package/dist/commands/index.js.map +1 -0
  106. package/dist/core/Actor.d.ts +103 -0
  107. package/dist/core/Actor.d.ts.map +1 -0
  108. package/dist/core/Actor.js +409 -0
  109. package/dist/core/Actor.js.map +1 -0
  110. package/dist/core/Combat.d.ts +37 -0
  111. package/dist/core/Combat.d.ts.map +1 -0
  112. package/dist/core/Combat.js +294 -0
  113. package/dist/core/Combat.js.map +1 -0
  114. package/dist/core/DungeonRunner.d.ts +169 -0
  115. package/dist/core/DungeonRunner.d.ts.map +1 -0
  116. package/dist/core/DungeonRunner.js +627 -0
  117. package/dist/core/DungeonRunner.js.map +1 -0
  118. package/dist/core/Game.d.ts +133 -0
  119. package/dist/core/Game.d.ts.map +1 -0
  120. package/dist/core/Game.js +644 -0
  121. package/dist/core/Game.js.map +1 -0
  122. package/dist/core/IdleCombat.d.ts +61 -0
  123. package/dist/core/IdleCombat.d.ts.map +1 -0
  124. package/dist/core/IdleCombat.js +461 -0
  125. package/dist/core/IdleCombat.js.map +1 -0
  126. package/dist/core/IdleGameManager.d.ts +198 -0
  127. package/dist/core/IdleGameManager.d.ts.map +1 -0
  128. package/dist/core/IdleGameManager.js +688 -0
  129. package/dist/core/IdleGameManager.js.map +1 -0
  130. package/dist/core/IdleSaveManager.d.ts +109 -0
  131. package/dist/core/IdleSaveManager.d.ts.map +1 -0
  132. package/dist/core/IdleSaveManager.js +296 -0
  133. package/dist/core/IdleSaveManager.js.map +1 -0
  134. package/dist/core/NewGameFlowManager.d.ts +64 -0
  135. package/dist/core/NewGameFlowManager.d.ts.map +1 -0
  136. package/dist/core/NewGameFlowManager.js +153 -0
  137. package/dist/core/NewGameFlowManager.js.map +1 -0
  138. package/dist/core/Player.d.ts +65 -0
  139. package/dist/core/Player.d.ts.map +1 -0
  140. package/dist/core/Player.js +261 -0
  141. package/dist/core/Player.js.map +1 -0
  142. package/dist/core/RoomHandlers.d.ts +75 -0
  143. package/dist/core/RoomHandlers.d.ts.map +1 -0
  144. package/dist/core/RoomHandlers.js +383 -0
  145. package/dist/core/RoomHandlers.js.map +1 -0
  146. package/dist/core/SaveManager.d.ts +84 -0
  147. package/dist/core/SaveManager.d.ts.map +1 -0
  148. package/dist/core/SaveManager.js +281 -0
  149. package/dist/core/SaveManager.js.map +1 -0
  150. package/dist/core/SaveMigration.d.ts +69 -0
  151. package/dist/core/SaveMigration.d.ts.map +1 -0
  152. package/dist/core/SaveMigration.js +408 -0
  153. package/dist/core/SaveMigration.js.map +1 -0
  154. package/dist/core/StateAdapter.d.ts +79 -0
  155. package/dist/core/StateAdapter.d.ts.map +1 -0
  156. package/dist/core/StateAdapter.js +397 -0
  157. package/dist/core/StateAdapter.js.map +1 -0
  158. package/dist/core/Team.d.ts +145 -0
  159. package/dist/core/Team.d.ts.map +1 -0
  160. package/dist/core/Team.js +371 -0
  161. package/dist/core/Team.js.map +1 -0
  162. package/dist/core/TeamCombat.d.ts +88 -0
  163. package/dist/core/TeamCombat.d.ts.map +1 -0
  164. package/dist/core/TeamCombat.js +405 -0
  165. package/dist/core/TeamCombat.js.map +1 -0
  166. package/dist/core/TeamDungeonRunner.d.ts +186 -0
  167. package/dist/core/TeamDungeonRunner.d.ts.map +1 -0
  168. package/dist/core/TeamDungeonRunner.js +758 -0
  169. package/dist/core/TeamDungeonRunner.js.map +1 -0
  170. package/dist/core/TimeManager.d.ts +114 -0
  171. package/dist/core/TimeManager.d.ts.map +1 -0
  172. package/dist/core/TimeManager.js +318 -0
  173. package/dist/core/TimeManager.js.map +1 -0
  174. package/dist/core/index.d.ts +9 -0
  175. package/dist/core/index.d.ts.map +1 -0
  176. package/dist/core/index.js +12 -0
  177. package/dist/core/index.js.map +1 -0
  178. package/dist/core/timeConstants.d.ts +135 -0
  179. package/dist/core/timeConstants.d.ts.map +1 -0
  180. package/dist/core/timeConstants.js +157 -0
  181. package/dist/core/timeConstants.js.map +1 -0
  182. package/dist/core/types.d.ts +780 -0
  183. package/dist/core/types.d.ts.map +1 -0
  184. package/dist/core/types.js +16 -0
  185. package/dist/core/types.js.map +1 -0
  186. package/dist/data/continents/index.d.ts +163 -0
  187. package/dist/data/continents/index.d.ts.map +1 -0
  188. package/dist/data/continents/index.js +31 -0
  189. package/dist/data/continents/index.js.map +1 -0
  190. package/dist/data/continents/verdantia.d.ts +294 -0
  191. package/dist/data/continents/verdantia.d.ts.map +1 -0
  192. package/dist/data/continents/verdantia.js +327 -0
  193. package/dist/data/continents/verdantia.js.map +1 -0
  194. package/dist/handlers/DialogHandler.d.ts +95 -0
  195. package/dist/handlers/DialogHandler.d.ts.map +1 -0
  196. package/dist/handlers/DialogHandler.js +450 -0
  197. package/dist/handlers/DialogHandler.js.map +1 -0
  198. package/dist/handlers/SaveLoadHandler.d.ts +60 -0
  199. package/dist/handlers/SaveLoadHandler.d.ts.map +1 -0
  200. package/dist/handlers/SaveLoadHandler.js +187 -0
  201. package/dist/handlers/SaveLoadHandler.js.map +1 -0
  202. package/dist/handlers/TitleScreenHandler.d.ts +43 -0
  203. package/dist/handlers/TitleScreenHandler.d.ts.map +1 -0
  204. package/dist/handlers/TitleScreenHandler.js +508 -0
  205. package/dist/handlers/TitleScreenHandler.js.map +1 -0
  206. package/dist/handlers/WorkshopHandler.d.ts +75 -0
  207. package/dist/handlers/WorkshopHandler.d.ts.map +1 -0
  208. package/dist/handlers/WorkshopHandler.js +401 -0
  209. package/dist/handlers/WorkshopHandler.js.map +1 -0
  210. package/dist/handlers/index.d.ts +12 -0
  211. package/dist/handlers/index.d.ts.map +1 -0
  212. package/dist/handlers/index.js +14 -0
  213. package/dist/handlers/index.js.map +1 -0
  214. package/dist/handlers/types.d.ts +34 -0
  215. package/dist/handlers/types.d.ts.map +1 -0
  216. package/dist/handlers/types.js +8 -0
  217. package/dist/handlers/types.js.map +1 -0
  218. package/dist/i18n/en.d.ts +3 -0
  219. package/dist/i18n/en.d.ts.map +1 -0
  220. package/dist/i18n/en.js +130 -0
  221. package/dist/i18n/en.js.map +1 -0
  222. package/dist/i18n/index.d.ts +40 -0
  223. package/dist/i18n/index.d.ts.map +1 -0
  224. package/dist/i18n/index.js +105 -0
  225. package/dist/i18n/index.js.map +1 -0
  226. package/dist/i18n/types.d.ts +133 -0
  227. package/dist/i18n/types.d.ts.map +1 -0
  228. package/dist/i18n/types.js +8 -0
  229. package/dist/i18n/types.js.map +1 -0
  230. package/dist/i18n/zh.d.ts +3 -0
  231. package/dist/i18n/zh.d.ts.map +1 -0
  232. package/dist/i18n/zh.js +130 -0
  233. package/dist/i18n/zh.js.map +1 -0
  234. package/dist/instrument.d.ts +8 -0
  235. package/dist/instrument.d.ts.map +1 -0
  236. package/dist/instrument.js +33 -0
  237. package/dist/instrument.js.map +1 -0
  238. package/dist/main-new.d.ts +12 -0
  239. package/dist/main-new.d.ts.map +1 -0
  240. package/dist/main-new.js +32 -0
  241. package/dist/main-new.js.map +1 -0
  242. package/dist/main.d.ts +7 -0
  243. package/dist/main.d.ts.map +1 -0
  244. package/dist/main.js +2816 -0
  245. package/dist/main.js.map +1 -0
  246. package/dist/map/ContinentManager.d.ts +88 -0
  247. package/dist/map/ContinentManager.d.ts.map +1 -0
  248. package/dist/map/ContinentManager.js +241 -0
  249. package/dist/map/ContinentManager.js.map +1 -0
  250. package/dist/map/DungeonGenerator.d.ts +32 -0
  251. package/dist/map/DungeonGenerator.d.ts.map +1 -0
  252. package/dist/map/DungeonGenerator.js +615 -0
  253. package/dist/map/DungeonGenerator.js.map +1 -0
  254. package/dist/map/MapGenerator.d.ts +27 -0
  255. package/dist/map/MapGenerator.d.ts.map +1 -0
  256. package/dist/map/MapGenerator.js +485 -0
  257. package/dist/map/MapGenerator.js.map +1 -0
  258. package/dist/map/index.d.ts +5 -0
  259. package/dist/map/index.d.ts.map +1 -0
  260. package/dist/map/index.js +8 -0
  261. package/dist/map/index.js.map +1 -0
  262. package/dist/npc/NPCGenerator.d.ts +68 -0
  263. package/dist/npc/NPCGenerator.d.ts.map +1 -0
  264. package/dist/npc/NPCGenerator.js +468 -0
  265. package/dist/npc/NPCGenerator.js.map +1 -0
  266. package/dist/npc/NPCManager.d.ts +86 -0
  267. package/dist/npc/NPCManager.d.ts.map +1 -0
  268. package/dist/npc/NPCManager.js +217 -0
  269. package/dist/npc/NPCManager.js.map +1 -0
  270. package/dist/npc/fixedNPCs.d.ts +7 -0
  271. package/dist/npc/fixedNPCs.d.ts.map +1 -0
  272. package/dist/npc/fixedNPCs.js +196 -0
  273. package/dist/npc/fixedNPCs.js.map +1 -0
  274. package/dist/npc/index.d.ts +9 -0
  275. package/dist/npc/index.d.ts.map +1 -0
  276. package/dist/npc/index.js +12 -0
  277. package/dist/npc/index.js.map +1 -0
  278. package/dist/npc/traits.d.ts +33 -0
  279. package/dist/npc/traits.d.ts.map +1 -0
  280. package/dist/npc/traits.js +795 -0
  281. package/dist/npc/traits.js.map +1 -0
  282. package/dist/npc/types.d.ts +193 -0
  283. package/dist/npc/types.d.ts.map +1 -0
  284. package/dist/npc/types.js +9 -0
  285. package/dist/npc/types.js.map +1 -0
  286. package/dist/quest/QuestManager.d.ts +79 -0
  287. package/dist/quest/QuestManager.d.ts.map +1 -0
  288. package/dist/quest/QuestManager.js +273 -0
  289. package/dist/quest/QuestManager.js.map +1 -0
  290. package/dist/quest/index.d.ts +6 -0
  291. package/dist/quest/index.d.ts.map +1 -0
  292. package/dist/quest/index.js +9 -0
  293. package/dist/quest/index.js.map +1 -0
  294. package/dist/quest/types.d.ts +81 -0
  295. package/dist/quest/types.d.ts.map +1 -0
  296. package/dist/quest/types.js +8 -0
  297. package/dist/quest/types.js.map +1 -0
  298. package/dist/strategy/StrategyExecutor.d.ts +159 -0
  299. package/dist/strategy/StrategyExecutor.d.ts.map +1 -0
  300. package/dist/strategy/StrategyExecutor.js +479 -0
  301. package/dist/strategy/StrategyExecutor.js.map +1 -0
  302. package/dist/strategy/StrategyParser.d.ts +48 -0
  303. package/dist/strategy/StrategyParser.d.ts.map +1 -0
  304. package/dist/strategy/StrategyParser.js +321 -0
  305. package/dist/strategy/StrategyParser.js.map +1 -0
  306. package/dist/strategy/defaultStrategy.d.ts +40 -0
  307. package/dist/strategy/defaultStrategy.d.ts.map +1 -0
  308. package/dist/strategy/defaultStrategy.js +254 -0
  309. package/dist/strategy/defaultStrategy.js.map +1 -0
  310. package/dist/strategy/index.d.ts +8 -0
  311. package/dist/strategy/index.d.ts.map +1 -0
  312. package/dist/strategy/index.js +14 -0
  313. package/dist/strategy/index.js.map +1 -0
  314. package/dist/tui/ExploreMenu.d.ts +106 -0
  315. package/dist/tui/ExploreMenu.d.ts.map +1 -0
  316. package/dist/tui/ExploreMenu.js +282 -0
  317. package/dist/tui/ExploreMenu.js.map +1 -0
  318. package/dist/tui/GameUI.d.ts +313 -0
  319. package/dist/tui/GameUI.d.ts.map +1 -0
  320. package/dist/tui/GameUI.js +2116 -0
  321. package/dist/tui/GameUI.js.map +1 -0
  322. package/dist/tui/GameUIAdapter.d.ts +207 -0
  323. package/dist/tui/GameUIAdapter.d.ts.map +1 -0
  324. package/dist/tui/GameUIAdapter.js +1342 -0
  325. package/dist/tui/GameUIAdapter.js.map +1 -0
  326. package/dist/tui/Input.d.ts +139 -0
  327. package/dist/tui/Input.d.ts.map +1 -0
  328. package/dist/tui/Input.js +278 -0
  329. package/dist/tui/Input.js.map +1 -0
  330. package/dist/tui/Menu.d.ts +110 -0
  331. package/dist/tui/Menu.d.ts.map +1 -0
  332. package/dist/tui/Menu.js +365 -0
  333. package/dist/tui/Menu.js.map +1 -0
  334. package/dist/tui/Screen.d.ts +228 -0
  335. package/dist/tui/Screen.d.ts.map +1 -0
  336. package/dist/tui/Screen.js +502 -0
  337. package/dist/tui/Screen.js.map +1 -0
  338. package/dist/tui/components/Box.d.ts +36 -0
  339. package/dist/tui/components/Box.d.ts.map +1 -0
  340. package/dist/tui/components/Box.js +43 -0
  341. package/dist/tui/components/Box.js.map +1 -0
  342. package/dist/tui/components/List.d.ts +69 -0
  343. package/dist/tui/components/List.d.ts.map +1 -0
  344. package/dist/tui/components/List.js +136 -0
  345. package/dist/tui/components/List.js.map +1 -0
  346. package/dist/tui/components/ProgressBar.d.ts +42 -0
  347. package/dist/tui/components/ProgressBar.d.ts.map +1 -0
  348. package/dist/tui/components/ProgressBar.js +75 -0
  349. package/dist/tui/components/ProgressBar.js.map +1 -0
  350. package/dist/tui/components/index.d.ts +8 -0
  351. package/dist/tui/components/index.d.ts.map +1 -0
  352. package/dist/tui/components/index.js +11 -0
  353. package/dist/tui/components/index.js.map +1 -0
  354. package/dist/tui/core/BaseSection.d.ts +98 -0
  355. package/dist/tui/core/BaseSection.d.ts.map +1 -0
  356. package/dist/tui/core/BaseSection.js +174 -0
  357. package/dist/tui/core/BaseSection.js.map +1 -0
  358. package/dist/tui/core/Component.d.ts +61 -0
  359. package/dist/tui/core/Component.d.ts.map +1 -0
  360. package/dist/tui/core/Component.js +32 -0
  361. package/dist/tui/core/Component.js.map +1 -0
  362. package/dist/tui/core/Section.d.ts +101 -0
  363. package/dist/tui/core/Section.d.ts.map +1 -0
  364. package/dist/tui/core/Section.js +24 -0
  365. package/dist/tui/core/Section.js.map +1 -0
  366. package/dist/tui/core/SectionManager.d.ts +108 -0
  367. package/dist/tui/core/SectionManager.d.ts.map +1 -0
  368. package/dist/tui/core/SectionManager.js +258 -0
  369. package/dist/tui/core/SectionManager.js.map +1 -0
  370. package/dist/tui/core/index.d.ts +9 -0
  371. package/dist/tui/core/index.d.ts.map +1 -0
  372. package/dist/tui/core/index.js +12 -0
  373. package/dist/tui/core/index.js.map +1 -0
  374. package/dist/tui/index.d.ts +15 -0
  375. package/dist/tui/index.d.ts.map +1 -0
  376. package/dist/tui/index.js +23 -0
  377. package/dist/tui/index.js.map +1 -0
  378. package/dist/tui/screens/BaseScreen.d.ts +62 -0
  379. package/dist/tui/screens/BaseScreen.d.ts.map +1 -0
  380. package/dist/tui/screens/BaseScreen.js +55 -0
  381. package/dist/tui/screens/BaseScreen.js.map +1 -0
  382. package/dist/tui/screens/CombatScreen.d.ts +43 -0
  383. package/dist/tui/screens/CombatScreen.d.ts.map +1 -0
  384. package/dist/tui/screens/CombatScreen.js +125 -0
  385. package/dist/tui/screens/CombatScreen.js.map +1 -0
  386. package/dist/tui/screens/DialogScreen.d.ts +53 -0
  387. package/dist/tui/screens/DialogScreen.d.ts.map +1 -0
  388. package/dist/tui/screens/DialogScreen.js +90 -0
  389. package/dist/tui/screens/DialogScreen.js.map +1 -0
  390. package/dist/tui/screens/DungeonScreen.d.ts +80 -0
  391. package/dist/tui/screens/DungeonScreen.d.ts.map +1 -0
  392. package/dist/tui/screens/DungeonScreen.js +317 -0
  393. package/dist/tui/screens/DungeonScreen.js.map +1 -0
  394. package/dist/tui/screens/ExploreScreen.d.ts +69 -0
  395. package/dist/tui/screens/ExploreScreen.d.ts.map +1 -0
  396. package/dist/tui/screens/ExploreScreen.js +224 -0
  397. package/dist/tui/screens/ExploreScreen.js.map +1 -0
  398. package/dist/tui/screens/SectionScreen.d.ts +84 -0
  399. package/dist/tui/screens/SectionScreen.d.ts.map +1 -0
  400. package/dist/tui/screens/SectionScreen.js +156 -0
  401. package/dist/tui/screens/SectionScreen.js.map +1 -0
  402. package/dist/tui/screens/TitleScreen.d.ts +40 -0
  403. package/dist/tui/screens/TitleScreen.d.ts.map +1 -0
  404. package/dist/tui/screens/TitleScreen.js +253 -0
  405. package/dist/tui/screens/TitleScreen.js.map +1 -0
  406. package/dist/tui/screens/TownScreen.d.ts +98 -0
  407. package/dist/tui/screens/TownScreen.d.ts.map +1 -0
  408. package/dist/tui/screens/TownScreen.js +370 -0
  409. package/dist/tui/screens/TownScreen.js.map +1 -0
  410. package/dist/tui/screens/TravelScreen.d.ts +67 -0
  411. package/dist/tui/screens/TravelScreen.d.ts.map +1 -0
  412. package/dist/tui/screens/TravelScreen.js +286 -0
  413. package/dist/tui/screens/TravelScreen.js.map +1 -0
  414. package/dist/tui/screens/index.d.ts +8 -0
  415. package/dist/tui/screens/index.d.ts.map +1 -0
  416. package/dist/tui/screens/index.js +17 -0
  417. package/dist/tui/screens/index.js.map +1 -0
  418. package/dist/tui/sections/ActionsSection.d.ts +71 -0
  419. package/dist/tui/sections/ActionsSection.d.ts.map +1 -0
  420. package/dist/tui/sections/ActionsSection.js +184 -0
  421. package/dist/tui/sections/ActionsSection.js.map +1 -0
  422. package/dist/tui/sections/DungeonSection.d.ts +65 -0
  423. package/dist/tui/sections/DungeonSection.d.ts.map +1 -0
  424. package/dist/tui/sections/DungeonSection.js +144 -0
  425. package/dist/tui/sections/DungeonSection.js.map +1 -0
  426. package/dist/tui/sections/EventsSection.d.ts +50 -0
  427. package/dist/tui/sections/EventsSection.d.ts.map +1 -0
  428. package/dist/tui/sections/EventsSection.js +134 -0
  429. package/dist/tui/sections/EventsSection.js.map +1 -0
  430. package/dist/tui/sections/MapSection.d.ts +66 -0
  431. package/dist/tui/sections/MapSection.d.ts.map +1 -0
  432. package/dist/tui/sections/MapSection.js +669 -0
  433. package/dist/tui/sections/MapSection.js.map +1 -0
  434. package/dist/tui/sections/StatusSection.d.ts +47 -0
  435. package/dist/tui/sections/StatusSection.d.ts.map +1 -0
  436. package/dist/tui/sections/StatusSection.js +133 -0
  437. package/dist/tui/sections/StatusSection.js.map +1 -0
  438. package/dist/tui/sections/TeamSection.d.ts +71 -0
  439. package/dist/tui/sections/TeamSection.d.ts.map +1 -0
  440. package/dist/tui/sections/TeamSection.js +224 -0
  441. package/dist/tui/sections/TeamSection.js.map +1 -0
  442. package/dist/tui/sections/TravelingSection.d.ts +51 -0
  443. package/dist/tui/sections/TravelingSection.d.ts.map +1 -0
  444. package/dist/tui/sections/TravelingSection.js +106 -0
  445. package/dist/tui/sections/TravelingSection.js.map +1 -0
  446. package/dist/tui/sections/index.d.ts +9 -0
  447. package/dist/tui/sections/index.d.ts.map +1 -0
  448. package/dist/tui/sections/index.js +12 -0
  449. package/dist/tui/sections/index.js.map +1 -0
  450. package/dist/ui/Terminal.d.ts +68 -0
  451. package/dist/ui/Terminal.d.ts.map +1 -0
  452. package/dist/ui/Terminal.js +297 -0
  453. package/dist/ui/Terminal.js.map +1 -0
  454. package/dist/ui/index.d.ts +5 -0
  455. package/dist/ui/index.d.ts.map +1 -0
  456. package/dist/ui/index.js +8 -0
  457. package/dist/ui/index.js.map +1 -0
  458. package/dist/utils/configMigration.d.ts +14 -0
  459. package/dist/utils/configMigration.d.ts.map +1 -0
  460. package/dist/utils/configMigration.js +92 -0
  461. package/dist/utils/configMigration.js.map +1 -0
  462. package/dist/utils/errorHandler.d.ts +86 -0
  463. package/dist/utils/errorHandler.d.ts.map +1 -0
  464. package/dist/utils/errorHandler.js +224 -0
  465. package/dist/utils/errorHandler.js.map +1 -0
  466. package/dist/utils/logger.d.ts +14 -0
  467. package/dist/utils/logger.d.ts.map +1 -0
  468. package/dist/utils/logger.js +43 -0
  469. package/dist/utils/logger.js.map +1 -0
  470. package/dist/weapon/WeaponFactory.d.ts +32 -0
  471. package/dist/weapon/WeaponFactory.d.ts.map +1 -0
  472. package/dist/weapon/WeaponFactory.js +216 -0
  473. package/dist/weapon/WeaponFactory.js.map +1 -0
  474. package/dist/weapon/index.d.ts +5 -0
  475. package/dist/weapon/index.d.ts.map +1 -0
  476. package/dist/weapon/index.js +8 -0
  477. package/dist/weapon/index.js.map +1 -0
  478. package/package.json +46 -0
package/dist/main.js ADDED
@@ -0,0 +1,2816 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * IDLERPG.TERMINAL - Main Entry Point
4
+ * An AI-native game that lives in your terminal.
5
+ */
6
+
7
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="bfa08172-04b0-56af-9640-104baeb3ae89")}catch(e){}}();
8
+ import { getIdleGameManager } from './core/IdleGameManager.js';
9
+ import { adaptIdleGameStateToGameState } from './core/StateAdapter.js';
10
+ import { getRouter } from './app/Router.js';
11
+ import { getEventBus } from './app/EventBus.js';
12
+ import { getCommandRegistry } from './commands/index.js';
13
+ import { initiateLogin, completeLogin, generateQRCode, getAIClient, tryRestoreAuth, setNickname, getNickname, logout } from './ai/PlayKitClient.js';
14
+ import { getGameUI, getScreen, getInput, ANSI } from './tui/index.js';
15
+ import { getNPCManager } from './npc/NPCManager.js';
16
+ import { getQuestManager } from './quest/QuestManager.js';
17
+ import { getNPCGenerator } from './npc/NPCGenerator.js';
18
+ import { saveGame, loadGame, listSaveSlots, formatPlayTime, formatTimestamp } from './core/SaveManager.js';
19
+ import { getAffixRegistry } from './affix/AffixRegistry.js';
20
+ import { migrateConfigDirectory } from './utils/configMigration.js';
21
+ import { Sentry } from './instrument.js';
22
+ import { createActor } from './core/Actor.js';
23
+ import { createTeamWithActor } from './core/Team.js';
24
+ import { NewGameFlowManager } from './core/NewGameFlowManager.js';
25
+ import { TitleScreenHandler, DialogHandler, SaveLoadHandler, WorkshopHandler, } from './handlers/index.js';
26
+ // Run config migration early (before any config files are accessed)
27
+ migrateConfigDirectory();
28
+ // Spinner frames for loading animation
29
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
30
+ /**
31
+ * Main Application class
32
+ */
33
+ class IdleRPGTerminal {
34
+ idleGameManager;
35
+ ui = getGameUI();
36
+ screen = getScreen();
37
+ input = getInput();
38
+ npcManager = getNPCManager();
39
+ questManager = getQuestManager();
40
+ router = getRouter();
41
+ bus = getEventBus();
42
+ commandRegistry = getCommandRegistry();
43
+ isRunning = false;
44
+ isInFullScreenMode = false; // For character creation, prologue, etc.
45
+ // Handlers - extracted functionality for better organization
46
+ titleScreenHandler;
47
+ dialogHandler;
48
+ saveLoadHandler;
49
+ workshopHandler;
50
+ constructor() {
51
+ this.idleGameManager = getIdleGameManager();
52
+ // Initialize handlers
53
+ this.titleScreenHandler = new TitleScreenHandler(this.screen, this.input, () => this.cleanup());
54
+ this.dialogHandler = new DialogHandler({
55
+ ui: this.ui,
56
+ screen: this.screen,
57
+ input: this.input,
58
+ idleGameManager: this.idleGameManager,
59
+ router: this.router,
60
+ bus: this.bus,
61
+ npcManager: this.npcManager,
62
+ questManager: this.questManager,
63
+ addLog: (msg) => this.ui.addEventLog(msg),
64
+ });
65
+ this.saveLoadHandler = new SaveLoadHandler(this.idleGameManager, this.npcManager, this.questManager, (msg) => this.ui.addEventLog(msg));
66
+ this.workshopHandler = new WorkshopHandler(this.idleGameManager, (msg) => this.ui.addEventLog(msg));
67
+ this.setupRouterNavigation();
68
+ this.setupEventBusSubscriptions();
69
+ this.setupCommands();
70
+ }
71
+ /**
72
+ * Set up command registry with all game commands
73
+ */
74
+ setupCommands() {
75
+ // Create command context
76
+ const context = {
77
+ gameManager: this.idleGameManager,
78
+ router: this.router,
79
+ bus: this.bus,
80
+ log: (msg) => this.ui.addEventLog(msg),
81
+ };
82
+ this.commandRegistry.setContext(context);
83
+ // Register navigation commands
84
+ this.commandRegistry.registerAll([
85
+ {
86
+ name: 'map',
87
+ aliases: ['m'],
88
+ description: 'Show the map panel',
89
+ category: 'navigation',
90
+ execute: () => {
91
+ this.ui.addEventLog('Map is displayed on the left panel.');
92
+ },
93
+ },
94
+ {
95
+ name: 'travel',
96
+ aliases: ['t'],
97
+ description: 'Show available facilities',
98
+ category: 'navigation',
99
+ execute: () => this.handleTravelCommand(),
100
+ isAvailable: (ctx) => ctx.gameManager.getState()?.phase === 'in_town',
101
+ },
102
+ {
103
+ name: 'dungeon',
104
+ aliases: ['d'],
105
+ description: 'Show available dungeons',
106
+ category: 'navigation',
107
+ execute: () => this.handleDungeonCommand(),
108
+ isAvailable: (ctx) => ctx.gameManager.getState()?.phase === 'in_town',
109
+ },
110
+ {
111
+ name: 'npc',
112
+ aliases: ['n'],
113
+ description: 'Talk to NPCs',
114
+ category: 'navigation',
115
+ execute: () => this.handleNPCCommand(),
116
+ },
117
+ ]);
118
+ // Register system commands
119
+ this.commandRegistry.registerAll([
120
+ {
121
+ name: 'help',
122
+ aliases: ['h', '?'],
123
+ description: 'Show help',
124
+ category: 'system',
125
+ execute: () => this.showHelp(),
126
+ },
127
+ {
128
+ name: 'save',
129
+ description: 'Save the game',
130
+ category: 'system',
131
+ execute: () => this.saveLoadHandler.handleSaveCommand(),
132
+ },
133
+ {
134
+ name: 'load',
135
+ description: 'Load a saved game',
136
+ category: 'system',
137
+ execute: () => this.saveLoadHandler.handleLoadCommand(),
138
+ },
139
+ {
140
+ name: 'newgame',
141
+ description: 'Start a new game',
142
+ category: 'system',
143
+ execute: () => this.handleNewGameCommand(),
144
+ },
145
+ {
146
+ name: 'quit',
147
+ aliases: ['exit'],
148
+ description: 'Exit the game',
149
+ category: 'system',
150
+ execute: () => { this.isRunning = false; },
151
+ },
152
+ ]);
153
+ }
154
+ /**
155
+ * Set up EventBus subscriptions for cross-component communication
156
+ */
157
+ setupEventBusSubscriptions() {
158
+ // Subscribe to UI log events
159
+ this.bus.subscribe('ui:log', ({ message }) => {
160
+ this.ui.addEventLog(message);
161
+ });
162
+ }
163
+ /**
164
+ * Set up IdleGameManager callbacks with EventBus bridging
165
+ */
166
+ setupIdleGameCallbacks() {
167
+ this.idleGameManager.setCallbacks({
168
+ onPhaseChange: (phase, state) => {
169
+ this.bus.dispatch('idle:phaseChange', {
170
+ phase: phase,
171
+ town: state.currentTown,
172
+ dungeon: state.currentDungeon
173
+ });
174
+ this.onIdlePhaseChange(phase, state);
175
+ },
176
+ onPlayerUpdate: (p) => {
177
+ this.bus.dispatch('player:update', { player: p });
178
+ this.onIdlePlayerUpdate(p);
179
+ },
180
+ onDungeonProgress: (runnerState, combat) => {
181
+ this.bus.dispatch('idle:dungeonProgress', { runnerState, combat });
182
+ this.onDungeonProgress(runnerState, combat);
183
+ },
184
+ onEventLog: (message) => {
185
+ this.bus.log(message);
186
+ },
187
+ onTravelProgress: (progress, duration) => {
188
+ this.bus.dispatch('travel:progress', { progress, duration });
189
+ this.onTravelProgress(progress, duration);
190
+ },
191
+ onDungeonComplete: (success, rewards) => {
192
+ this.bus.dispatch('idle:dungeonComplete', { success, rewards });
193
+ this.onDungeonComplete(success, rewards);
194
+ },
195
+ onPlayerDeath: () => {
196
+ this.bus.dispatch('player:death', undefined);
197
+ this.onPlayerDeath();
198
+ },
199
+ });
200
+ }
201
+ /**
202
+ * Set up router navigation listener
203
+ * Maps route changes to UI mode changes
204
+ */
205
+ setupRouterNavigation() {
206
+ this.router.onNavigate((route, prev) => {
207
+ this.handleRouteChange(route, prev);
208
+ });
209
+ this.router.onBack((route) => {
210
+ this.handleRouteChange(route, null);
211
+ });
212
+ }
213
+ /**
214
+ * Handle route changes and update UI accordingly
215
+ */
216
+ handleRouteChange(route, prev) {
217
+ switch (route.type) {
218
+ case 'explore':
219
+ this.ui.setMode('explore');
220
+ // Clear any selection modes
221
+ this.npcSelectionMode = false;
222
+ this.dungeonSelectionMode = false;
223
+ break;
224
+ case 'dialog':
225
+ this.ui.setMode('dialog');
226
+ break;
227
+ case 'travel':
228
+ this.ui.setMode('travel');
229
+ break;
230
+ case 'combat':
231
+ this.ui.setMode('combat');
232
+ break;
233
+ case 'dungeon':
234
+ this.ui.setMode('explore');
235
+ this.ui.enterDungeonMode();
236
+ break;
237
+ case 'npcSelection':
238
+ // NPC selection is a sub-mode within explore
239
+ this.npcSelectionMode = true;
240
+ break;
241
+ case 'dungeonSelection':
242
+ // Dungeon selection is a sub-mode within explore
243
+ this.dungeonSelectionMode = true;
244
+ break;
245
+ case 'save':
246
+ // Save mode handled by SaveLoadHandler
247
+ break;
248
+ case 'load':
249
+ // Load mode handled by SaveLoadHandler
250
+ break;
251
+ case 'workshop':
252
+ // Workshop mode handled by WorkshopHandler
253
+ break;
254
+ case 'prologue':
255
+ case 'characterCreation':
256
+ this.isInFullScreenMode = true;
257
+ this.ui.pauseRendering();
258
+ break;
259
+ case 'dead':
260
+ // Dead state handled separately
261
+ break;
262
+ case 'gameover':
263
+ this.isRunning = false;
264
+ break;
265
+ }
266
+ // Handle leaving full-screen modes
267
+ if (prev?.type === 'prologue' || prev?.type === 'characterCreation') {
268
+ if (route.type !== 'prologue' && route.type !== 'characterCreation') {
269
+ this.isInFullScreenMode = false;
270
+ this.ui.resumeRendering();
271
+ }
272
+ }
273
+ }
274
+ // NPC interaction state
275
+ npcSelectionMode = false;
276
+ availableNPCs = [];
277
+ currentDialogNPC = null;
278
+ dialogHistory = [];
279
+ // Dungeon selection state
280
+ dungeonSelectionMode = false;
281
+ availableDungeons = [];
282
+ // AI error handling state
283
+ consecutiveAIFailures = 0;
284
+ MAX_CONSECUTIVE_FAILURES = 3;
285
+ /**
286
+ * Start the game
287
+ */
288
+ async start() {
289
+ try {
290
+ // Check terminal size first (before any UI)
291
+ await this.ensureMinimumTerminalSize();
292
+ // Show title screen and get player name (uses simple input, before full UI init)
293
+ const playerName = await this.showTitleScreen();
294
+ // Now initialize the full game UI (after title screen)
295
+ await this.ui.initialize();
296
+ // Initialize systems
297
+ await this.initializeSystems();
298
+ // Set up callbacks with EventBus bridging
299
+ this.setupIdleGameCallbacks();
300
+ // Check for existing saves to determine flow
301
+ const hasSaves = await this.checkForExistingSaves();
302
+ if (hasSaves) {
303
+ // Returning player: Initialize with their name (save will be loaded via menu)
304
+ this.initializeGame(playerName);
305
+ this.ui.addEventLog(`Welcome back, ${playerName}!`);
306
+ this.ui.addEventLog(`Type 'load' to continue your adventure, or 'newgame' to start fresh.`);
307
+ }
308
+ else {
309
+ // New player: Run the full New Game flow
310
+ const flowResult = await this.runNewGameFlowAndStart();
311
+ if (!flowResult) {
312
+ // User cancelled - exit gracefully
313
+ this.cleanup();
314
+ process.exit(0);
315
+ }
316
+ }
317
+ // Set up command and event handlers
318
+ this.ui.onCommandInput((cmd) => this.handleCommand(cmd));
319
+ this.setupEventHandlers();
320
+ // Start main loop
321
+ await this.mainLoop();
322
+ }
323
+ catch (error) {
324
+ this.cleanup();
325
+ console.error('Fatal error:', error);
326
+ process.exit(1);
327
+ }
328
+ }
329
+ /**
330
+ * Check if any save files exist
331
+ */
332
+ async checkForExistingSaves() {
333
+ const slots = await listSaveSlots();
334
+ return slots.some(s => s.exists);
335
+ }
336
+ /**
337
+ * Run the New Game flow and initialize the game
338
+ * @returns true if successful, false if cancelled
339
+ */
340
+ async runNewGameFlowAndStart() {
341
+ this.isInFullScreenMode = true;
342
+ this.ui.pauseRendering(); // Stop GameUIAdapter refresh to prevent screen flicker
343
+ try {
344
+ const flowManager = new NewGameFlowManager(this.screen, this.input);
345
+ const result = await flowManager.run();
346
+ if (result.success && result.actor) {
347
+ // Create team and initialize game
348
+ const team = createTeamWithActor(result.actor, `${result.actor.name}'s Team`);
349
+ const actors = { [result.actor.id]: result.actor };
350
+ this.idleGameManager.initializeWithTeam(team, actors);
351
+ // Clear screen and show welcome
352
+ this.screen.clear();
353
+ this.ui.addEventLog(`══════════════════════════════════════`);
354
+ this.ui.addEventLog(` Welcome, ${result.actor.name} the ${result.actor.role}!`);
355
+ this.ui.addEventLog(` Your adventure begins...`);
356
+ this.ui.addEventLog(`══════════════════════════════════════`);
357
+ return true;
358
+ }
359
+ return false;
360
+ }
361
+ finally {
362
+ this.ui.resumeRendering(); // Always restore GameUIAdapter refresh
363
+ this.isInFullScreenMode = false;
364
+ }
365
+ }
366
+ /**
367
+ * Create a new team with the starting actor
368
+ * Team system is the core game design - always enabled
369
+ */
370
+ createTeamWithStartingActor(name) {
371
+ // Create the first actor with balanced role
372
+ const actor = createActor(name, 'balanced');
373
+ // Create a team with this actor
374
+ const team = createTeamWithActor(actor, `${name}'s Team`);
375
+ // Store actors in a record
376
+ const actors = { [actor.id]: actor };
377
+ return { team, actors };
378
+ }
379
+ /**
380
+ * Initialize the game with team mode (always enabled)
381
+ */
382
+ initializeGame(playerName) {
383
+ const { team, actors } = this.createTeamWithStartingActor(playerName);
384
+ this.idleGameManager.initializeWithTeam(team, actors);
385
+ }
386
+ /**
387
+ * Ensure terminal meets minimum size requirements
388
+ * Waits for user to resize if too small
389
+ */
390
+ async ensureMinimumTerminalSize() {
391
+ // Enter alternate screen buffer to avoid overlapping with terminal history
392
+ this.screen.enterAlternateScreen();
393
+ // Hide cursor during size check
394
+ this.screen.hideCursor();
395
+ const check = this.screen.checkMinimumSize();
396
+ if (check.valid) {
397
+ return; // Size is OK
398
+ }
399
+ // Show warning and wait for resize
400
+ this.screen.renderSizeWarning();
401
+ // Wait for resize to valid size
402
+ return new Promise((resolve) => {
403
+ const checkAndResolve = (width, height) => {
404
+ const newCheck = this.screen.checkMinimumSize();
405
+ if (newCheck.valid) {
406
+ // Clear the warning
407
+ this.screen.clear();
408
+ this.screen.forceRedraw();
409
+ resolve();
410
+ }
411
+ else {
412
+ // Still too small, show warning again
413
+ this.screen.renderSizeWarning();
414
+ }
415
+ };
416
+ // Register resize callback
417
+ this.screen.onResize(checkAndResolve);
418
+ });
419
+ }
420
+ /**
421
+ * Wait for Enter key to start login
422
+ */
423
+ async waitForLoginKey() {
424
+ return new Promise((resolve) => {
425
+ const handler = (key) => {
426
+ if (key.name === 'return') {
427
+ this.input.offKey(handler);
428
+ resolve();
429
+ }
430
+ else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
431
+ this.cleanup();
432
+ process.exit(0);
433
+ }
434
+ };
435
+ this.input.onKey(handler);
436
+ });
437
+ }
438
+ /**
439
+ * Show "Press Enter to start" and wait for Enter key
440
+ * Features: blinking text, Ctrl+C graceful exit, Ctrl+O logout
441
+ */
442
+ async waitForGameStart(playerName) {
443
+ const { width } = this.screen.getSize();
444
+ const centerX = Math.floor(width / 2);
445
+ this.screen.clear();
446
+ const logoY = this.drawAsciiTitle();
447
+ const contentY = logoY + 3;
448
+ // Show welcome message
449
+ const welcomeMsg = `Welcome, ${playerName}!`;
450
+ this.screen.write(centerX - Math.floor(welcomeMsg.length / 2), contentY, welcomeMsg, ANSI.fg.green);
451
+ // Show "Press Enter to start" with blinking effect
452
+ const startMsg = '▶ Press ENTER to Start ◀';
453
+ const startMsgX = centerX - Math.floor(startMsg.length / 2);
454
+ const startMsgY = contentY + 3;
455
+ let blinkVisible = true;
456
+ // Blinking interval
457
+ const blinkInterval = setInterval(() => {
458
+ if (blinkVisible) {
459
+ this.screen.write(startMsgX, startMsgY, startMsg, ANSI.fg.cyan);
460
+ }
461
+ else {
462
+ this.screen.write(startMsgX, startMsgY, ' '.repeat(startMsg.length), ANSI.fg.cyan);
463
+ }
464
+ blinkVisible = !blinkVisible;
465
+ this.screen.forceRedraw();
466
+ }, 600);
467
+ // Initial draw
468
+ this.screen.write(startMsgX, startMsgY, startMsg, ANSI.fg.cyan);
469
+ // Show hint for logout
470
+ const logoutHint = 'Ctrl+O to logout';
471
+ this.screen.write(centerX - Math.floor(logoutHint.length / 2), startMsgY + 3, logoutHint, ANSI.fg.gray);
472
+ this.screen.forceRedraw();
473
+ // Wait for Enter
474
+ return new Promise((resolve) => {
475
+ const handler = async (key) => {
476
+ if (key.name === 'return') {
477
+ clearInterval(blinkInterval);
478
+ this.input.offKey(handler);
479
+ resolve();
480
+ }
481
+ else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
482
+ clearInterval(blinkInterval);
483
+ this.input.offKey(handler);
484
+ this.cleanup();
485
+ process.exit(0);
486
+ }
487
+ else if (key.ctrl && key.name === 'o') {
488
+ // Logout
489
+ clearInterval(blinkInterval);
490
+ this.input.offKey(handler);
491
+ await this.handleLogout();
492
+ }
493
+ };
494
+ this.input.onKey(handler);
495
+ });
496
+ }
497
+ /**
498
+ * Handle logout from title screen
499
+ */
500
+ async handleLogout() {
501
+ const { width } = this.screen.getSize();
502
+ const centerX = Math.floor(width / 2);
503
+ this.screen.clear();
504
+ const logoY = this.drawAsciiTitle();
505
+ const contentY = logoY + 3;
506
+ this.screen.write(centerX - 10, contentY, 'Logging out...', ANSI.fg.yellow);
507
+ this.screen.forceRedraw();
508
+ try {
509
+ await logout();
510
+ this.screen.write(centerX - 15, contentY + 2, '✓ Logged out successfully', ANSI.fg.green);
511
+ this.screen.forceRedraw();
512
+ await this.sleep(1000);
513
+ // Restart the title screen flow
514
+ const playerName = await this.showTitleScreen();
515
+ // Continue with game start
516
+ await this.continueAfterLogin(playerName);
517
+ }
518
+ catch (error) {
519
+ this.screen.write(centerX - 10, contentY + 2, '✗ Logout failed', ANSI.fg.red);
520
+ this.screen.forceRedraw();
521
+ await this.sleep(1500);
522
+ // Restart anyway
523
+ const playerName = await this.showTitleScreen();
524
+ await this.continueAfterLogin(playerName);
525
+ }
526
+ }
527
+ /**
528
+ * Continue game after login (extracted to avoid duplication)
529
+ */
530
+ async continueAfterLogin(playerName) {
531
+ // Now initialize the full game UI (after title screen)
532
+ await this.ui.initialize();
533
+ // Initialize systems
534
+ await this.initializeSystems();
535
+ // Set up callbacks with EventBus bridging
536
+ this.setupIdleGameCallbacks();
537
+ // Initialize game (team or legacy mode based on USE_TEAM_MODE)
538
+ this.initializeGame(playerName);
539
+ this.ui.addEventLog(`Welcome to ${this.idleGameManager.getState()?.currentTown.name || 'town'}!`);
540
+ this.ui.addEventLog(`Your adventure begins, ${playerName}...`);
541
+ // Set up command and event handlers
542
+ this.ui.onCommandInput((cmd) => this.handleCommand(cmd));
543
+ this.setupEventHandlers();
544
+ // Start main loop
545
+ await this.mainLoop();
546
+ }
547
+ // ASCII Art Logo
548
+ static ASCII_LOGO = [
549
+ '██╗██████╗ ██╗ ███████╗██████╗ ██████╗ ██████╗ ',
550
+ '██║██╔══██╗██║ ██╔════╝██╔══██╗██╔══██╗██╔════╝ ',
551
+ '██║██║ ██║██║ █████╗ ██████╔╝██████╔╝██║ ███╗',
552
+ '██║██║ ██║██║ ██╔══╝ ██╔══██╗██╔═══╝ ██║ ██║',
553
+ '██║██████╔╝███████╗███████╗██║ ██║██║ ╚██████╔╝',
554
+ '╚═╝╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═════╝ ',
555
+ ];
556
+ static ASCII_TERMINAL = [
557
+ '████████╗███████╗██████╗ ███╗ ███╗██╗███╗ ██╗ █████╗ ██╗ ',
558
+ '╚══██╔══╝██╔════╝██╔══██╗████╗ ████║██║████╗ ██║██╔══██╗██║ ',
559
+ ' ██║ █████╗ ██████╔╝██╔████╔██║██║██╔██╗ ██║███████║██║ ',
560
+ ' ██║ ██╔══╝ ██╔══██╗██║╚██╔╝██║██║██║╚██╗██║██╔══██║██║ ',
561
+ ' ██║ ███████╗██║ ██║██║ ╚═╝ ██║██║██║ ╚████║██║ ██║███████╗',
562
+ ' ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝',
563
+ ];
564
+ static ASCII_COMPACT = [
565
+ ' ╔═══════════════════════════════════════╗',
566
+ ' ║ █ ████ █ █████ ████ ████ ██ ║',
567
+ ' ║ █ █ █ █ █ █ █ █ █ █ ║',
568
+ ' ║ █ █ █ █ ████ ████ ████ █ ██ ║',
569
+ ' ║ █ █ █ █ █ █ █ █ █ █ ║',
570
+ ' ║ █ ████ █████ █████ █ █ █ ██ ║',
571
+ ' ╠═══════════════════════════════════════╣',
572
+ ' ║ T E R M I N A L v0.1.0 ║',
573
+ ' ╚═══════════════════════════════════════╝',
574
+ ];
575
+ /**
576
+ * Draw ASCII art title
577
+ */
578
+ drawAsciiTitle() {
579
+ const { width, height } = this.screen.getSize();
580
+ // Use compact logo for smaller terminals
581
+ const useCompact = width < 75;
582
+ const logo = useCompact ? IdleRPGTerminal.ASCII_COMPACT : IdleRPGTerminal.ASCII_LOGO;
583
+ const logoWidth = logo[0].length;
584
+ const logoX = Math.floor((width - logoWidth) / 2);
585
+ let y = 2;
586
+ // Draw ASCII logo
587
+ for (const line of logo) {
588
+ this.screen.write(Math.max(1, logoX), y++, line, ANSI.fg.cyan);
589
+ }
590
+ // Draw TERMINAL text (if using full logo)
591
+ if (!useCompact) {
592
+ y++;
593
+ const termX = Math.floor((width - IdleRPGTerminal.ASCII_TERMINAL[0].length) / 2);
594
+ for (const line of IdleRPGTerminal.ASCII_TERMINAL) {
595
+ this.screen.write(Math.max(1, termX), y++, line, ANSI.fg.yellow);
596
+ }
597
+ }
598
+ return y;
599
+ }
600
+ /**
601
+ * Show title screen and get player name with login flow
602
+ * Flow: Try restore auth -> if success, welcome back | else Enter -> show URL | Enter again -> open browser | Q -> show QR code (hide title)
603
+ */
604
+ async showTitleScreen() {
605
+ const { width, height } = this.screen.getSize();
606
+ this.screen.clear();
607
+ // Draw ASCII art
608
+ let y = this.drawAsciiTitle();
609
+ y += 2;
610
+ const centerX = Math.floor(width / 2);
611
+ // Tagline
612
+ const tagline = '~ An AI-native adventure that lives in your terminal ~';
613
+ this.screen.write(centerX - Math.floor(tagline.length / 2), y++, tagline, ANSI.fg.gray);
614
+ y++;
615
+ // Decorative line
616
+ const lineWidth = Math.min(50, width - 10);
617
+ const lineX = Math.floor((width - lineWidth) / 2);
618
+ this.screen.write(lineX, y++, '═'.repeat(lineWidth), ANSI.fg.gray);
619
+ y++;
620
+ const loginY = y;
621
+ // Start input (needed for key detection)
622
+ this.input.start();
623
+ this.input.setMode('raw');
624
+ // Show "Checking authentication..." while trying to restore
625
+ this.screen.write(centerX - 15, loginY, 'Checking authentication...', ANSI.fg.yellow);
626
+ this.screen.forceRedraw();
627
+ // Try to restore authentication from saved tokens
628
+ const restoreResult = await tryRestoreAuth();
629
+ if (restoreResult.success) {
630
+ // Authentication restored successfully!
631
+ this.screen.clear();
632
+ this.drawAsciiTitle();
633
+ const msg = restoreResult.needsRefresh
634
+ ? '✓ Session refreshed!'
635
+ : '✓ Welcome back!';
636
+ this.screen.write(centerX - 12, loginY, msg, ANSI.fg.green);
637
+ this.screen.forceRedraw();
638
+ // Get nickname from PlayKit
639
+ const nickname = await getNickname();
640
+ if (nickname) {
641
+ // Has nickname, wait for Enter to start game
642
+ await this.waitForGameStart(nickname);
643
+ return nickname;
644
+ }
645
+ else {
646
+ // No nickname set, prompt user to set one
647
+ this.screen.write(centerX - 15, loginY + 2, 'You need to set a nickname first', ANSI.fg.yellow);
648
+ this.screen.forceRedraw();
649
+ await this.sleep(1000);
650
+ const newNickname = await this.promptSetNickname();
651
+ await this.waitForGameStart(newNickname);
652
+ return newNickname;
653
+ }
654
+ }
655
+ // No saved auth or restore failed - show login UI
656
+ this.screen.clear();
657
+ this.drawAsciiTitle();
658
+ // Show "Press ENTER to login" message
659
+ this.screen.write(centerX - 15, loginY, 'Login required to play', ANSI.fg.gray);
660
+ this.screen.write(centerX - 15, loginY + 2, '▶ Press ENTER to Login ◀', ANSI.fg.cyan);
661
+ this.screen.write(centerX - 10, loginY + 4, 'Esc to quit', ANSI.fg.gray);
662
+ this.screen.forceRedraw();
663
+ // Wait for Enter key
664
+ await this.waitForLoginKey();
665
+ // Now start the login process
666
+ let playerName = '';
667
+ let spinnerFrame = 0;
668
+ let spinnerInterval = null;
669
+ let showingQR = false;
670
+ let qrCode = '';
671
+ const startSpinner = () => {
672
+ spinnerInterval = setInterval(() => {
673
+ spinnerFrame = (spinnerFrame + 1) % SPINNER_FRAMES.length;
674
+ }, 100);
675
+ };
676
+ const stopSpinner = () => {
677
+ if (spinnerInterval) {
678
+ clearInterval(spinnerInterval);
679
+ spinnerInterval = null;
680
+ }
681
+ };
682
+ const getSpinner = () => SPINNER_FRAMES[spinnerFrame];
683
+ try {
684
+ // Step 1: Show "Preparing login..." with spinner
685
+ this.screen.clear();
686
+ this.drawAsciiTitle();
687
+ this.screen.write(centerX - 15, loginY, `${getSpinner()} Preparing login...`, ANSI.fg.yellow);
688
+ this.screen.forceRedraw();
689
+ startSpinner();
690
+ // Initiate login to get URL
691
+ const loginInfo = await initiateLogin();
692
+ // Generate QR code in background (for when user presses Q)
693
+ qrCode = await generateQRCode(loginInfo.authUrl);
694
+ // Helper to redraw the waiting screen
695
+ const redrawWaitingScreen = () => {
696
+ this.screen.clear();
697
+ let contentY = 2;
698
+ if (!showingQR) {
699
+ // Show with title
700
+ contentY = this.drawAsciiTitle();
701
+ contentY += 2;
702
+ // Show URL
703
+ this.screen.write(centerX - 10, contentY++, 'Please visit:', ANSI.fg.white);
704
+ contentY++;
705
+ const url = loginInfo.authUrl;
706
+ const maxLen = width - 10;
707
+ const displayUrl = url.length > maxLen ? url.slice(0, maxLen - 3) + '...' : url;
708
+ this.screen.write(centerX - Math.floor(displayUrl.length / 2), contentY++, displayUrl, ANSI.fg.cyan);
709
+ contentY += 2;
710
+ // Status line
711
+ this.screen.write(centerX - 18, contentY, `${getSpinner()} Waiting for authentication...`, ANSI.fg.yellow);
712
+ contentY += 2;
713
+ // Instructions
714
+ this.screen.write(centerX - 20, contentY++, 'Enter: Open in browser', ANSI.fg.gray);
715
+ this.screen.write(centerX - 20, contentY++, 'Q: Show QR code (hide title)', ANSI.fg.gray);
716
+ this.screen.write(centerX - 20, contentY++, 'Esc: Cancel', ANSI.fg.gray);
717
+ }
718
+ else {
719
+ // Show QR code only (no title, more space)
720
+ contentY = 1;
721
+ // Draw QR code
722
+ const qrLines = qrCode.split('\n');
723
+ for (const line of qrLines) {
724
+ if (line.trim()) {
725
+ this.screen.write(centerX - Math.floor(line.length / 2), contentY++, line, ANSI.fg.white);
726
+ }
727
+ }
728
+ contentY++;
729
+ // URL below QR
730
+ const url = loginInfo.authUrl;
731
+ const maxLen = width - 4;
732
+ const displayUrl = url.length > maxLen ? url.slice(0, maxLen - 3) + '...' : url;
733
+ this.screen.write(centerX - Math.floor(displayUrl.length / 2), contentY++, displayUrl, ANSI.fg.cyan);
734
+ contentY += 2;
735
+ // Status
736
+ this.screen.write(centerX - 18, contentY, `${getSpinner()} Waiting for authentication...`, ANSI.fg.yellow);
737
+ contentY += 2;
738
+ // Instructions
739
+ this.screen.write(centerX - 20, contentY++, 'Enter: Open in browser', ANSI.fg.gray);
740
+ this.screen.write(centerX - 20, contentY++, 'Q: Show title (hide QR)', ANSI.fg.gray);
741
+ this.screen.write(centerX - 20, contentY++, 'Esc: Cancel', ANSI.fg.gray);
742
+ }
743
+ this.screen.forceRedraw();
744
+ };
745
+ // Initial draw (showing URL, not QR)
746
+ redrawWaitingScreen();
747
+ // Set up key handler
748
+ let cancelled = false;
749
+ let exitRequested = false;
750
+ let spinnerUpdateInterval = null;
751
+ const keyHandler = (key) => {
752
+ if (key.name === 'return') {
753
+ // Open URL in browser
754
+ import('open').then(({ default: openUrl }) => {
755
+ openUrl(loginInfo.authUrl).catch(() => { });
756
+ }).catch(() => { });
757
+ }
758
+ else if (key.raw === 'q' || key.raw === 'Q') {
759
+ // Toggle QR code display
760
+ showingQR = !showingQR;
761
+ redrawWaitingScreen();
762
+ }
763
+ else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
764
+ cancelled = true;
765
+ exitRequested = true;
766
+ // Graceful exit
767
+ if (spinnerUpdateInterval)
768
+ clearInterval(spinnerUpdateInterval);
769
+ this.input.offKey(keyHandler);
770
+ stopSpinner();
771
+ this.cleanup();
772
+ process.exit(0);
773
+ }
774
+ };
775
+ this.input.onKey(keyHandler);
776
+ // Spinner update loop - only update the spinner character, don't redraw everything
777
+ spinnerUpdateInterval = setInterval(() => {
778
+ if (cancelled)
779
+ return;
780
+ // Just redraw the entire screen with updated spinner
781
+ redrawWaitingScreen();
782
+ }, 100);
783
+ // Wait for login completion
784
+ try {
785
+ const result = await completeLogin((status) => {
786
+ if (cancelled)
787
+ return;
788
+ if (status === 'completed') {
789
+ if (spinnerUpdateInterval)
790
+ clearInterval(spinnerUpdateInterval);
791
+ this.input.offKey(keyHandler);
792
+ stopSpinner();
793
+ // Show success
794
+ this.screen.clear();
795
+ this.drawAsciiTitle();
796
+ this.screen.write(centerX - 12, loginY, '✓ Login successful!', ANSI.fg.green);
797
+ this.screen.forceRedraw();
798
+ }
799
+ });
800
+ if (spinnerUpdateInterval)
801
+ clearInterval(spinnerUpdateInterval);
802
+ this.input.offKey(keyHandler);
803
+ stopSpinner();
804
+ if (cancelled) {
805
+ throw new Error('Login cancelled');
806
+ }
807
+ // Show success
808
+ this.screen.clear();
809
+ this.drawAsciiTitle();
810
+ this.screen.write(centerX - 12, loginY, '✓ Login successful!', ANSI.fg.green);
811
+ this.screen.forceRedraw();
812
+ // Get nickname from PlayKit (may be set from previous sessions)
813
+ const nickname = await getNickname();
814
+ if (nickname) {
815
+ playerName = nickname;
816
+ }
817
+ else {
818
+ // No nickname set, prompt user to set one
819
+ this.screen.write(centerX - 15, loginY + 2, 'Set your adventurer name', ANSI.fg.yellow);
820
+ this.screen.forceRedraw();
821
+ await this.sleep(1000);
822
+ playerName = await this.promptSetNickname();
823
+ }
824
+ // Wait for Enter to start game
825
+ await this.waitForGameStart(playerName);
826
+ }
827
+ catch (loginError) {
828
+ if (spinnerUpdateInterval)
829
+ clearInterval(spinnerUpdateInterval);
830
+ this.input.offKey(keyHandler);
831
+ stopSpinner();
832
+ throw loginError;
833
+ }
834
+ }
835
+ catch (error) {
836
+ stopSpinner();
837
+ // Show error
838
+ this.screen.clear();
839
+ this.drawAsciiTitle();
840
+ this.screen.write(centerX - 10, loginY, '✗ Login failed', ANSI.fg.red);
841
+ this.screen.write(centerX - Math.floor((error.message || 'Unknown error').length / 2), loginY + 2, error.message || 'Unknown error', ANSI.fg.gray);
842
+ this.screen.write(centerX - 15, loginY + 4, 'Press any key to exit...', ANSI.fg.yellow);
843
+ this.screen.forceRedraw();
844
+ await this.input.waitForKey();
845
+ this.cleanup();
846
+ process.exit(1);
847
+ }
848
+ // If we got a player name from auth, use it; otherwise prompt
849
+ if (playerName) {
850
+ return playerName;
851
+ }
852
+ // Fallback: prompt to set nickname (this shouldn't normally be reached)
853
+ const fallbackName = await this.promptSetNickname();
854
+ await this.waitForGameStart(fallbackName);
855
+ return fallbackName;
856
+ }
857
+ /**
858
+ * Prompt user to set their nickname (saved to PlayKit)
859
+ */
860
+ async promptSetNickname() {
861
+ const { width, height } = this.screen.getSize();
862
+ const centerX = Math.floor(width / 2);
863
+ const maxNameLength = 16; // PlayKit limit is 1-16 characters
864
+ // Calculate position
865
+ const useCompact = width < 75;
866
+ const inputY = useCompact ? 14 : 20;
867
+ let inputBuffer = '';
868
+ let errorMessage = '';
869
+ const drawPrompt = () => {
870
+ this.screen.clear();
871
+ this.drawAsciiTitle();
872
+ const promptText = 'Set your adventurer name:';
873
+ this.screen.write(centerX - Math.floor(promptText.length / 2), inputY, promptText, ANSI.fg.white);
874
+ // Rules hint
875
+ const rulesText = '(1-16 chars, letters, numbers, Chinese, _, space)';
876
+ this.screen.write(centerX - Math.floor(rulesText.length / 2), inputY + 1, rulesText, ANSI.fg.gray);
877
+ // Input box
878
+ const inputWidth = 24;
879
+ const inputX = Math.floor((width - inputWidth) / 2);
880
+ this.screen.write(inputX, inputY + 3, '╔' + '═'.repeat(inputWidth - 2) + '╗', ANSI.fg.cyan);
881
+ this.screen.write(inputX, inputY + 4, '║ ', ANSI.fg.cyan);
882
+ this.screen.write(inputX + 2, inputY + 4, inputBuffer.slice(0, maxNameLength).padEnd(inputWidth - 4) + '▌', ANSI.fg.white);
883
+ this.screen.write(inputX + inputWidth - 1, inputY + 4, '║', ANSI.fg.cyan);
884
+ this.screen.write(inputX, inputY + 5, '╚' + '═'.repeat(inputWidth - 2) + '╝', ANSI.fg.cyan);
885
+ // Error message
886
+ if (errorMessage) {
887
+ this.screen.write(centerX - Math.floor(errorMessage.length / 2), inputY + 7, errorMessage, ANSI.fg.red);
888
+ }
889
+ // Instructions
890
+ this.screen.write(centerX - 15, inputY + 9, 'Enter to confirm Esc to use default', ANSI.fg.gray);
891
+ this.screen.forceRedraw();
892
+ };
893
+ drawPrompt();
894
+ this.screen.showCursor();
895
+ return new Promise((resolve) => {
896
+ let isProcessing = false; // Prevent double-processing
897
+ const handleKeypress = (key) => {
898
+ // Ignore input while processing async operation
899
+ if (isProcessing)
900
+ return;
901
+ if (key.name === 'return') {
902
+ const trimmed = inputBuffer.trim();
903
+ if (!trimmed) {
904
+ errorMessage = 'Name cannot be empty';
905
+ drawPrompt();
906
+ return;
907
+ }
908
+ // Mark as processing to prevent double-submit
909
+ isProcessing = true;
910
+ // Try to set nickname on PlayKit
911
+ this.screen.write(centerX - 10, inputY + 7, 'Saving... ', ANSI.fg.yellow);
912
+ this.screen.forceRedraw();
913
+ setNickname(trimmed).then((result) => {
914
+ if (result.success && result.nickname) {
915
+ this.input.offKey(handleKeypress);
916
+ this.screen.hideCursor();
917
+ // Show success
918
+ this.screen.write(centerX - 15, inputY + 7, `✓ Welcome, ${result.nickname}!`.padEnd(30), ANSI.fg.green);
919
+ this.screen.forceRedraw();
920
+ setTimeout(() => {
921
+ resolve(result.nickname);
922
+ }, 1500);
923
+ }
924
+ else {
925
+ isProcessing = false;
926
+ errorMessage = result.error || 'Failed to set name';
927
+ drawPrompt();
928
+ }
929
+ }).catch((err) => {
930
+ isProcessing = false;
931
+ errorMessage = 'Network error, please try again';
932
+ drawPrompt();
933
+ });
934
+ }
935
+ else if (key.name === 'backspace') {
936
+ inputBuffer = inputBuffer.slice(0, -1);
937
+ errorMessage = '';
938
+ drawPrompt();
939
+ }
940
+ else if (key.name === 'escape') {
941
+ // Use default name
942
+ this.input.offKey(handleKeypress);
943
+ this.screen.hideCursor();
944
+ resolve('Adventurer');
945
+ }
946
+ else if (key.raw && !key.ctrl && !key.meta) {
947
+ const charCode = key.raw.charCodeAt(0);
948
+ if (charCode >= 32 && inputBuffer.length < maxNameLength) {
949
+ inputBuffer += key.raw;
950
+ errorMessage = '';
951
+ drawPrompt();
952
+ }
953
+ }
954
+ };
955
+ this.input.onKey(handleKeypress);
956
+ });
957
+ }
958
+ /**
959
+ * Get player name input on title screen
960
+ * Self-contained input handling that doesn't rely on GameUI
961
+ */
962
+ async getTitleScreenInput() {
963
+ const { width, height } = this.screen.getSize();
964
+ const centerX = Math.floor(width / 2);
965
+ const maxNameLength = 20;
966
+ // Calculate input position based on ASCII art
967
+ const getInputY = () => {
968
+ const { width: w } = this.screen.getSize();
969
+ const useCompact = w < 75;
970
+ // ASCII logo height + TERMINAL + spacing
971
+ return useCompact ? 14 : 20;
972
+ };
973
+ // Draw prompt
974
+ const drawPrompt = (currentInput) => {
975
+ const { width: w } = this.screen.getSize();
976
+ const cx = Math.floor(w / 2);
977
+ const inputY = getInputY();
978
+ this.screen.write(cx - 15, inputY, 'What is your name, adventurer?', ANSI.fg.white);
979
+ // Input box
980
+ const inputWidth = 24;
981
+ const inputX = Math.floor((w - inputWidth) / 2);
982
+ this.screen.write(inputX, inputY + 2, '╔' + '═'.repeat(inputWidth - 2) + '╗', ANSI.fg.cyan);
983
+ this.screen.write(inputX, inputY + 3, '║ ', ANSI.fg.cyan);
984
+ this.screen.write(inputX + 2, inputY + 3, currentInput.slice(0, maxNameLength).padEnd(inputWidth - 4) + '▌', ANSI.fg.white);
985
+ this.screen.write(inputX + inputWidth - 1, inputY + 3, '║', ANSI.fg.cyan);
986
+ this.screen.write(inputX, inputY + 4, '╚' + '═'.repeat(inputWidth - 2) + '╝', ANSI.fg.cyan);
987
+ this.screen.write(cx - 15, inputY + 6, 'Enter to confirm Esc to quit', ANSI.fg.gray);
988
+ };
989
+ // Full redraw for title screen (used after resize)
990
+ const redrawTitleScreen = (currentInput) => {
991
+ const { width: w } = this.screen.getSize();
992
+ const cx = Math.floor(w / 2);
993
+ this.screen.clear();
994
+ // Draw ASCII art
995
+ this.drawAsciiTitle();
996
+ // Tagline
997
+ const tagline = '~ An AI-native adventure that lives in your terminal ~';
998
+ const useCompact = w < 75;
999
+ const tagY = useCompact ? 12 : 16;
1000
+ this.screen.write(cx - Math.floor(tagline.length / 2), tagY, tagline, ANSI.fg.gray);
1001
+ drawPrompt(currentInput);
1002
+ this.screen.forceRedraw();
1003
+ };
1004
+ // Initial draw
1005
+ drawPrompt('');
1006
+ this.screen.showCursor();
1007
+ this.screen.forceRedraw();
1008
+ // Set up resize handler for title screen
1009
+ let inputBuffer = '';
1010
+ const resizeHandler = () => {
1011
+ const sizeCheck = this.screen.checkMinimumSize();
1012
+ if (!sizeCheck.valid) {
1013
+ this.screen.renderSizeWarning();
1014
+ }
1015
+ else {
1016
+ redrawTitleScreen(inputBuffer);
1017
+ }
1018
+ };
1019
+ this.screen.onResize(resizeHandler);
1020
+ // Start input
1021
+ this.input.start();
1022
+ return new Promise((resolve) => {
1023
+ const handleKeypress = (key) => {
1024
+ if (key.name === 'return') {
1025
+ // Enter pressed - submit
1026
+ this.input.offKey(handleKeypress);
1027
+ this.screen.hideCursor();
1028
+ resolve(inputBuffer.trim() || 'Wanderer');
1029
+ }
1030
+ else if (key.name === 'backspace') {
1031
+ // Backspace - delete last char
1032
+ inputBuffer = inputBuffer.slice(0, -1);
1033
+ drawPrompt(inputBuffer);
1034
+ this.screen.render();
1035
+ }
1036
+ else if (key.name === 'escape') {
1037
+ // Escape - clear input
1038
+ inputBuffer = '';
1039
+ drawPrompt(inputBuffer);
1040
+ this.screen.render();
1041
+ }
1042
+ else if (key.raw && !key.ctrl && !key.meta) {
1043
+ // Printable character
1044
+ const charCode = key.raw.charCodeAt(0);
1045
+ if (charCode >= 32 && inputBuffer.length < maxNameLength) {
1046
+ inputBuffer += key.raw;
1047
+ drawPrompt(inputBuffer);
1048
+ this.screen.render();
1049
+ }
1050
+ }
1051
+ };
1052
+ this.input.onKey(handleKeypress);
1053
+ });
1054
+ }
1055
+ /**
1056
+ * Initialize game systems
1057
+ */
1058
+ async initializeSystems() {
1059
+ // Systems are initialized as singletons when accessed
1060
+ // NPC Manager already loads fixed NPCs
1061
+ // Quest Manager is ready
1062
+ }
1063
+ /**
1064
+ * Set up event handlers (called from start())
1065
+ */
1066
+ setupEventHandlers() {
1067
+ // Character edit handler
1068
+ this.ui.onCharacterEditHandler((field, value) => this.handleCharacterEdit(field, value));
1069
+ // Cancel travel handler
1070
+ this.ui.setOnCancelTravel(() => this.cancelTravel());
1071
+ // Abort dungeon handler
1072
+ this.ui.setOnAbortDungeon(() => this.abortDungeon());
1073
+ // Process signals - cleanup and exit gracefully
1074
+ process.on('SIGINT', () => {
1075
+ this.cleanup();
1076
+ process.exit(0);
1077
+ });
1078
+ process.on('SIGTERM', () => {
1079
+ this.cleanup();
1080
+ process.exit(0);
1081
+ });
1082
+ }
1083
+ /**
1084
+ * Handle character customization edits
1085
+ */
1086
+ async handleCharacterEdit(field, value) {
1087
+ const idleState = this.idleGameManager.getState();
1088
+ if (!idleState)
1089
+ return;
1090
+ if (field === 'name') {
1091
+ // Update player name both locally and on PlayKit
1092
+ idleState.player.name = value;
1093
+ // Try to update nickname on PlayKit
1094
+ const result = await setNickname(value);
1095
+ if (result.success) {
1096
+ this.ui.addEventLog(`✓ Name changed to "${value}"`);
1097
+ }
1098
+ else {
1099
+ // Name was updated locally, but PlayKit sync failed
1100
+ this.ui.addEventLog(`✓ Name changed to "${value}"`);
1101
+ if (result.error) {
1102
+ this.ui.addEventLog(`⚠ Online sync failed: ${result.error}`);
1103
+ }
1104
+ }
1105
+ }
1106
+ else {
1107
+ // Update character customization field
1108
+ const characterUpdate = {};
1109
+ characterUpdate[field] = value;
1110
+ if (idleState.player.character) {
1111
+ Object.assign(idleState.player.character, characterUpdate);
1112
+ }
1113
+ const fieldLabels = {
1114
+ backstory: 'Background Story',
1115
+ appearance: 'Appearance',
1116
+ personality: 'Personality',
1117
+ motivation: 'Motivation',
1118
+ origin: 'Origin',
1119
+ };
1120
+ const label = fieldLabels[field] || field;
1121
+ this.ui.addEventLog(`✓ ${label} updated`);
1122
+ }
1123
+ }
1124
+ // ==========================================================================
1125
+ // IdleGameManager Callbacks
1126
+ // ==========================================================================
1127
+ onIdlePhaseChange(phase, state) {
1128
+ switch (phase) {
1129
+ case 'in_town':
1130
+ case 'resting':
1131
+ this.router.navigate({ type: 'explore' });
1132
+ break;
1133
+ case 'traveling_to_dungeon':
1134
+ this.router.navigate({
1135
+ type: 'travel',
1136
+ targetRoomId: state.currentDungeon?.id || '',
1137
+ phase: 'traveling_to_dungeon'
1138
+ });
1139
+ break;
1140
+ case 'returning_to_town':
1141
+ this.router.navigate({
1142
+ type: 'travel',
1143
+ targetRoomId: state.currentTown.id,
1144
+ phase: 'returning_to_town'
1145
+ });
1146
+ break;
1147
+ case 'in_dungeon':
1148
+ const runnerState = state.dungeonRunner?.getState();
1149
+ if (runnerState?.activeCombat) {
1150
+ this.router.navigate({ type: 'combat', encounterId: 'current' });
1151
+ }
1152
+ else {
1153
+ this.router.navigate({ type: 'dungeon', dungeonId: state.currentDungeon?.id || '' });
1154
+ }
1155
+ break;
1156
+ case 'dead':
1157
+ this.router.navigate({ type: 'dead' });
1158
+ this.ui.addEventLog('You have died...');
1159
+ break;
1160
+ }
1161
+ }
1162
+ onIdlePlayerUpdate(player) {
1163
+ // Player stats updated, UI will refresh on next render
1164
+ }
1165
+ onDungeonProgress(runnerState, combat) {
1166
+ const idleState = this.idleGameManager.getState();
1167
+ if (combat) {
1168
+ this.router.navigate({ type: 'combat', encounterId: 'current' });
1169
+ }
1170
+ else if (idleState?.currentDungeon) {
1171
+ this.router.navigate({ type: 'dungeon', dungeonId: idleState.currentDungeon.id });
1172
+ }
1173
+ }
1174
+ onTravelProgress(progress, duration) {
1175
+ // Travel progress updated, UI will refresh on next render
1176
+ }
1177
+ onDungeonComplete(success, rewards) {
1178
+ if (success) {
1179
+ this.ui.addEventLog(`Dungeon cleared! Gained ${rewards.gold} gold and ${rewards.exp} exp.`);
1180
+ if (rewards.items.length > 0) {
1181
+ this.ui.addEventLog(`Found: ${rewards.items.join(', ')}`);
1182
+ }
1183
+ }
1184
+ else {
1185
+ this.ui.addEventLog('Dungeon run failed.');
1186
+ }
1187
+ }
1188
+ onPlayerDeath() {
1189
+ this.ui.addEventLog('Press R to respawn at town, or Q to quit.');
1190
+ }
1191
+ /**
1192
+ * Main game loop
1193
+ */
1194
+ async mainLoop() {
1195
+ this.isRunning = true;
1196
+ this.router.navigate({ type: 'explore' });
1197
+ while (this.isRunning) {
1198
+ // Skip normal rendering when in full screen mode (character creation, etc.)
1199
+ if (this.isInFullScreenMode) {
1200
+ await this.sleep(50);
1201
+ continue;
1202
+ }
1203
+ const idleState = this.idleGameManager.getState();
1204
+ if (!idleState)
1205
+ break;
1206
+ // Convert IdleGameState to GameState for UI
1207
+ const state = adaptIdleGameStateToGameState(idleState);
1208
+ // Check for daily death check
1209
+ if (this.npcManager.shouldPerformDeathCheck()) {
1210
+ const result = this.npcManager.performDailyDeathCheck();
1211
+ if (result.died.length > 0) {
1212
+ for (const npc of result.died) {
1213
+ this.ui.addEventLog(`${npc.name} has died: ${npc.deathCause}`);
1214
+ }
1215
+ }
1216
+ }
1217
+ // Check quest timeouts
1218
+ const failedQuests = this.questManager.checkTimeouts();
1219
+ for (const quest of failedQuests) {
1220
+ this.ui.addEventLog(`Quest failed: ${quest.name}`);
1221
+ }
1222
+ // Render based on current phase
1223
+ switch (idleState.phase) {
1224
+ case 'in_town':
1225
+ case 'resting':
1226
+ this.renderExploreMode(state);
1227
+ break;
1228
+ case 'traveling_to_dungeon':
1229
+ case 'returning_to_town':
1230
+ this.renderTravelMode(state);
1231
+ break;
1232
+ case 'in_dungeon':
1233
+ if (state.phase === 'combat' && state.combat) {
1234
+ this.renderCombatMode(state);
1235
+ }
1236
+ else {
1237
+ this.renderDungeonMode(state);
1238
+ }
1239
+ break;
1240
+ case 'dead':
1241
+ this.renderDeadScreen(state);
1242
+ break;
1243
+ }
1244
+ // Small delay to prevent CPU spinning
1245
+ await this.sleep(50);
1246
+ }
1247
+ this.cleanup();
1248
+ }
1249
+ /**
1250
+ * Render dead screen
1251
+ */
1252
+ renderDeadScreen(state) {
1253
+ const { width, height } = this.screen.getSize();
1254
+ const centerX = Math.floor(width / 2);
1255
+ const centerY = Math.floor(height / 2);
1256
+ this.screen.clear();
1257
+ this.screen.write(centerX - 8, centerY - 2, 'YOU HAVE DIED', ANSI.fg.red, undefined, ANSI.bold);
1258
+ this.screen.write(centerX - 15, centerY + 1, 'Press [R] to respawn at town', ANSI.fg.gray);
1259
+ this.screen.write(centerX - 10, centerY + 2, 'Press [Q] to quit', ANSI.fg.gray);
1260
+ this.screen.render();
1261
+ }
1262
+ /**
1263
+ * Render explore mode
1264
+ */
1265
+ renderExploreMode(state) {
1266
+ const idleState = this.idleGameManager.getState();
1267
+ // If not in traveling state, ensure we exit travel mode
1268
+ if (idleState && idleState.phase !== 'traveling_to_dungeon' && idleState.phase !== 'returning_to_town') {
1269
+ this.ui.exitTravelMode();
1270
+ this.ui.clearTravelState();
1271
+ }
1272
+ // If not in dungeon, ensure we exit dungeon mode
1273
+ if (idleState && idleState.phase !== 'in_dungeon') {
1274
+ this.ui.exitDungeonMode();
1275
+ }
1276
+ const npcsInRoom = this.npcManager.getByRoom(state.currentRoom.id);
1277
+ const activeQuests = this.questManager.getActiveQuests();
1278
+ this.ui.renderMainScreen(state, npcsInRoom, activeQuests);
1279
+ }
1280
+ /**
1281
+ * Render travel mode - uses main explore layout with TravelingSection
1282
+ */
1283
+ renderTravelMode(state) {
1284
+ if (!state.travelState)
1285
+ return;
1286
+ const idleState = this.idleGameManager.getState();
1287
+ if (!idleState)
1288
+ return;
1289
+ const events = state.travelState.events.map(e => e.title);
1290
+ // Determine from/to names and room IDs for the player icon
1291
+ let fromName;
1292
+ let toName;
1293
+ let fromRoomId;
1294
+ let toRoomId;
1295
+ if (idleState.phase === 'traveling_to_dungeon') {
1296
+ // Traveling to dungeon: from town center to dungeon entrance
1297
+ fromName = idleState.currentTown.name;
1298
+ toName = idleState.currentDungeon?.name || 'Unknown Dungeon';
1299
+ fromRoomId = `${idleState.currentTown.id}_center`;
1300
+ // Find the dungeon entrance room in the map
1301
+ const dungeonId = idleState.currentDungeon?.id;
1302
+ toRoomId = fromRoomId; // Default fallback
1303
+ if (dungeonId) {
1304
+ for (const [id, room] of state.currentMap.rooms) {
1305
+ if (room.type === 'dungeon_entrance' && room.name.includes(dungeonId)) {
1306
+ toRoomId = id;
1307
+ break;
1308
+ }
1309
+ }
1310
+ }
1311
+ }
1312
+ else {
1313
+ // Returning to town: from dungeon to town center
1314
+ fromName = idleState.currentDungeon?.name || 'Dungeon';
1315
+ toName = idleState.currentTown.name;
1316
+ fromRoomId = `${idleState.currentTown.id}_center`; // Will be at town map
1317
+ toRoomId = `${idleState.currentTown.id}_center`;
1318
+ }
1319
+ // Ensure we're in travel mode
1320
+ this.ui.enterTravelMode();
1321
+ // Update travel state for UI
1322
+ this.ui.updateTravelState({
1323
+ fromRoomId: fromRoomId,
1324
+ toRoomId: toRoomId,
1325
+ fromName: fromName,
1326
+ toName: toName,
1327
+ progress: state.travelState.progress,
1328
+ duration: state.travelState.duration,
1329
+ events: events,
1330
+ phase: idleState.phase,
1331
+ });
1332
+ // Use main screen rendering (with TravelingSection visible)
1333
+ this.renderExploreMode(state);
1334
+ }
1335
+ /**
1336
+ * Render dungeon mode - uses dungeon layout with DungeonSection
1337
+ */
1338
+ renderDungeonMode(state) {
1339
+ const idleState = this.idleGameManager.getState();
1340
+ if (!idleState || !idleState.dungeonRunner)
1341
+ return;
1342
+ const runnerState = idleState.dungeonRunner.getState();
1343
+ if (!runnerState)
1344
+ return;
1345
+ // Ensure we're in dungeon mode
1346
+ this.ui.enterDungeonMode();
1347
+ // Calculate dungeon progress data
1348
+ const dungeon = idleState.currentDungeon;
1349
+ const totalRooms = dungeon ? Array.from(state.currentMap.rooms.values()).length : 0;
1350
+ const exploredRooms = runnerState.exploredRooms.size;
1351
+ // Calculate elapsed time from startTime
1352
+ const elapsedTime = (Date.now() - runnerState.startTime) / 1000; // seconds
1353
+ // Estimate time remaining based on progress
1354
+ const progressPct = totalRooms > 0 ? exploredRooms / totalRooms : 0;
1355
+ const estimatedTotal = progressPct > 0.1 ? elapsedTime / progressPct : (dungeon?.estimatedTime || 120);
1356
+ const estimatedRemaining = Math.max(0, estimatedTotal - elapsedTime);
1357
+ // Update dungeon section data
1358
+ this.ui.updateDungeonData({
1359
+ dungeonName: dungeon?.name || 'Unknown Dungeon',
1360
+ currentFloor: runnerState.currentLevel,
1361
+ totalFloors: dungeon?.levels.length || 1,
1362
+ roomsExplored: exploredRooms,
1363
+ totalRooms: totalRooms,
1364
+ enemiesDefeated: runnerState.enemiesKilled,
1365
+ totalEnemies: totalRooms, // Approximate
1366
+ treasuresFound: runnerState.lootCollected.length,
1367
+ currentRoomType: state.currentRoom.type,
1368
+ playerHp: state.player.stats.hp,
1369
+ playerMaxHp: state.player.stats.maxHp,
1370
+ estimatedTimeRemaining: estimatedRemaining,
1371
+ elapsedTime: elapsedTime,
1372
+ });
1373
+ // Render using main screen
1374
+ const npcsInRoom = this.npcManager.getByRoom(state.currentRoom.id);
1375
+ const activeQuests = this.questManager.getActiveQuests();
1376
+ this.ui.renderMainScreen(state, npcsInRoom, activeQuests);
1377
+ }
1378
+ /**
1379
+ * Render combat mode
1380
+ */
1381
+ renderCombatMode(state) {
1382
+ if (!state.combat)
1383
+ return;
1384
+ this.ui.renderCombatScreen(state.combat, state.player);
1385
+ }
1386
+ /**
1387
+ * Handle command input
1388
+ */
1389
+ handleCommand(cmd) {
1390
+ const idleState = this.idleGameManager.getState();
1391
+ if (!idleState)
1392
+ return;
1393
+ // Handle dead state special commands
1394
+ if (idleState.phase === 'dead') {
1395
+ if (cmd === 'r' || cmd === 'respawn') {
1396
+ this.idleGameManager.respawn();
1397
+ this.router.navigate({ type: 'explore' });
1398
+ return;
1399
+ }
1400
+ if (cmd === 'q' || cmd === 'quit') {
1401
+ this.router.navigate({ type: 'gameover' });
1402
+ return;
1403
+ }
1404
+ return; // Ignore other commands when dead
1405
+ }
1406
+ // Try registered commands first
1407
+ if (this.commandRegistry.execute(cmd)) {
1408
+ return;
1409
+ }
1410
+ // Handle special cases: numeric input and mode-specific commands
1411
+ if (!cmd)
1412
+ return;
1413
+ // Check if it's a number
1414
+ const num = parseInt(cmd, 10);
1415
+ if (!isNaN(num)) {
1416
+ // Handle based on current mode (using handlers)
1417
+ if (this.saveLoadHandler.handleNumericInput(num)) {
1418
+ return;
1419
+ }
1420
+ else if (this.workshopHandler.workshopMode) {
1421
+ this.workshopHandler.handleInput(num);
1422
+ return;
1423
+ }
1424
+ else if (this.npcSelectionMode) {
1425
+ this.handleNPCSelection(num);
1426
+ return;
1427
+ }
1428
+ else if (this.dungeonSelectionMode) {
1429
+ this.handleDungeonSelection(num);
1430
+ return;
1431
+ }
1432
+ else {
1433
+ // Handle map room selection (dungeon entrance, facilities)
1434
+ this.handleMapRoomSelection(num);
1435
+ return;
1436
+ }
1437
+ }
1438
+ // Non-numeric unknown command - cancel any pending modes or show error
1439
+ if (this.saveLoadHandler.saveMode) {
1440
+ this.saveLoadHandler.cancelSave();
1441
+ }
1442
+ else if (this.saveLoadHandler.loadMode) {
1443
+ this.saveLoadHandler.cancelLoad();
1444
+ }
1445
+ else if (this.workshopHandler.workshopMode) {
1446
+ this.workshopHandler.exit();
1447
+ }
1448
+ else if (this.dungeonSelectionMode) {
1449
+ this.dungeonSelectionMode = false;
1450
+ this.ui.addEventLog('Dungeon selection cancelled.');
1451
+ }
1452
+ else {
1453
+ this.ui.addEventLog(`Unknown command: ${cmd}. Type 'help' for commands.`);
1454
+ }
1455
+ }
1456
+ /**
1457
+ * Handle travel command - shows available facilities in town
1458
+ */
1459
+ handleTravelCommand() {
1460
+ const idleState = this.idleGameManager.getState();
1461
+ if (!idleState || idleState.phase !== 'in_town') {
1462
+ this.ui.addEventLog('You can only visit facilities when in town.');
1463
+ return;
1464
+ }
1465
+ const facilities = idleState.currentTown.facilities;
1466
+ if (facilities.length === 0) {
1467
+ this.ui.addEventLog('No facilities available in this town.');
1468
+ return;
1469
+ }
1470
+ this.ui.addEventLog('─── Town Facilities ───');
1471
+ facilities.forEach((facility, index) => {
1472
+ this.ui.addEventLog(` [${index + 1}] ${facility.name} (${facility.type})`);
1473
+ });
1474
+ this.ui.addEventLog('Enter a number to visit a facility.');
1475
+ }
1476
+ /**
1477
+ * Handle dungeon command - shows available dungeons
1478
+ */
1479
+ handleDungeonCommand() {
1480
+ const idleState = this.idleGameManager.getState();
1481
+ if (!idleState || idleState.phase !== 'in_town') {
1482
+ this.ui.addEventLog('You can only select dungeons when in town.');
1483
+ return;
1484
+ }
1485
+ this.availableDungeons = this.idleGameManager.getAvailableDungeons();
1486
+ if (this.availableDungeons.length === 0) {
1487
+ this.ui.addEventLog('No dungeons available from this town.');
1488
+ return;
1489
+ }
1490
+ this.dungeonSelectionMode = true;
1491
+ this.ui.addEventLog('─── Available Dungeons ───');
1492
+ this.availableDungeons.forEach((dungeon, index) => {
1493
+ const difficulty = dungeon.difficulty.charAt(0).toUpperCase() + dungeon.difficulty.slice(1);
1494
+ this.ui.addEventLog(` [${index + 1}] ${dungeon.name} (${difficulty}, Lv.${dungeon.recommendedLevel})`);
1495
+ });
1496
+ this.ui.addEventLog('Enter a number to travel to a dungeon, or any key to cancel.');
1497
+ }
1498
+ /**
1499
+ * Handle dungeon selection
1500
+ */
1501
+ handleDungeonSelection(num) {
1502
+ const index = num - 1;
1503
+ if (index >= 0 && index < this.availableDungeons.length) {
1504
+ const dungeon = this.availableDungeons[index];
1505
+ const success = this.idleGameManager.selectDungeon(dungeon.id);
1506
+ if (success) {
1507
+ this.router.navigate({
1508
+ type: 'travel',
1509
+ targetRoomId: dungeon.id,
1510
+ phase: 'traveling_to_dungeon'
1511
+ });
1512
+ this.ui.addEventLog(`Traveling to ${dungeon.name}...`);
1513
+ }
1514
+ else {
1515
+ this.ui.addEventLog('Cannot travel to that dungeon.');
1516
+ }
1517
+ }
1518
+ else {
1519
+ this.ui.addEventLog(`Invalid selection. Use 1-${this.availableDungeons.length}`);
1520
+ }
1521
+ this.dungeonSelectionMode = false;
1522
+ }
1523
+ /**
1524
+ * Handle room selection from map
1525
+ */
1526
+ handleMapRoomSelection(num) {
1527
+ const idleState = this.idleGameManager.getState();
1528
+ if (!idleState || idleState.phase !== 'in_town') {
1529
+ return;
1530
+ }
1531
+ // Get current room's connections
1532
+ const gameState = adaptIdleGameStateToGameState(idleState);
1533
+ const connections = gameState.currentRoom.connections;
1534
+ const index = num - 1;
1535
+ if (index < 0 || index >= connections.length) {
1536
+ return; // Invalid selection
1537
+ }
1538
+ const roomId = connections[index];
1539
+ const targetRoom = gameState.currentMap.rooms.get(roomId);
1540
+ if (!targetRoom) {
1541
+ return;
1542
+ }
1543
+ // Check if it's a dungeon entrance
1544
+ if (targetRoom.type === 'dungeon_entrance') {
1545
+ // Extract dungeonId from room name (format: "→ dungeonId")
1546
+ const dungeonId = targetRoom.name.replace('→ ', '').trim();
1547
+ const success = this.idleGameManager.selectDungeon(dungeonId);
1548
+ if (success) {
1549
+ this.router.navigate({
1550
+ type: 'travel',
1551
+ targetRoomId: dungeonId,
1552
+ phase: 'traveling_to_dungeon'
1553
+ });
1554
+ this.ui.addEventLog(`Traveling to dungeon: ${dungeonId}...`);
1555
+ }
1556
+ else {
1557
+ this.ui.addEventLog(`Cannot travel to ${dungeonId}`);
1558
+ }
1559
+ return;
1560
+ }
1561
+ // Handle facility access
1562
+ const facilities = idleState.currentTown.facilities;
1563
+ if (index < facilities.length) {
1564
+ const facility = facilities[index];
1565
+ this.idleGameManager.useFacility(facility);
1566
+ this.ui.addEventLog(`Visiting ${facility.name}...`);
1567
+ }
1568
+ }
1569
+ /**
1570
+ * Cancel current travel and return to town
1571
+ */
1572
+ cancelTravel() {
1573
+ const idleState = this.idleGameManager.getState();
1574
+ if (!idleState)
1575
+ return;
1576
+ if (idleState.phase === 'traveling_to_dungeon' || idleState.phase === 'returning_to_town') {
1577
+ // Cancel travel via IdleGameManager
1578
+ const success = this.idleGameManager.cancelTravel();
1579
+ if (success) {
1580
+ this.ui.exitTravelMode();
1581
+ this.ui.clearTravelState();
1582
+ this.router.navigate({ type: 'explore' });
1583
+ this.ui.addEventLog('Travel cancelled.');
1584
+ }
1585
+ }
1586
+ }
1587
+ /**
1588
+ * Abort current dungeon and return to town
1589
+ */
1590
+ abortDungeon() {
1591
+ const idleState = this.idleGameManager.getState();
1592
+ if (!idleState)
1593
+ return;
1594
+ if (idleState.phase === 'in_dungeon') {
1595
+ this.idleGameManager.abortDungeon();
1596
+ this.ui.exitDungeonMode();
1597
+ this.ui.addEventLog('Retreating from dungeon...');
1598
+ }
1599
+ }
1600
+ /**
1601
+ * Handle inventory command
1602
+ */
1603
+ handleInventoryCommand() {
1604
+ const player = this.idleGameManager.getState()?.player;
1605
+ if (!player)
1606
+ return;
1607
+ this.ui.addEventLog('─── Inventory ───');
1608
+ this.ui.addEventLog(`Gold: ${player.stats.gold}`);
1609
+ if (player.inventory.length === 0) {
1610
+ this.ui.addEventLog('(empty)');
1611
+ }
1612
+ else {
1613
+ for (const item of player.inventory.slice(0, 5)) {
1614
+ this.ui.addEventLog(`• ${item.name} x${item.quantity}`);
1615
+ }
1616
+ if (player.inventory.length > 5) {
1617
+ this.ui.addEventLog(`... and ${player.inventory.length - 5} more`);
1618
+ }
1619
+ }
1620
+ }
1621
+ /**
1622
+ * Handle character command
1623
+ */
1624
+ handleCharacterCommand() {
1625
+ const player = this.idleGameManager.getState()?.player;
1626
+ if (!player)
1627
+ return;
1628
+ const s = player.stats;
1629
+ this.ui.addEventLog(`─── ${player.name} ───`);
1630
+ this.ui.addEventLog(`Level ${s.level} | HP: ${s.hp}/${s.maxHp} | MP: ${s.mp}/${s.maxMp}`);
1631
+ this.ui.addEventLog(`ATK: ${s.attack} | DEF: ${s.defense} | SPD: ${s.speed}`);
1632
+ this.ui.addEventLog(`EXP: ${s.exp}/${s.expToNextLevel}`);
1633
+ }
1634
+ /**
1635
+ * Handle NPC command
1636
+ */
1637
+ handleNPCCommand() {
1638
+ const idleState = this.idleGameManager.getState();
1639
+ if (!idleState)
1640
+ return;
1641
+ // In town, use town center room ID for NPC lookup
1642
+ const roomId = idleState.phase === 'in_town'
1643
+ ? `${idleState.currentTown.id}_center`
1644
+ : '';
1645
+ const npcs = this.npcManager.getByRoom(roomId);
1646
+ if (npcs.length === 0) {
1647
+ // Check for encounter NPCs on current floor (floor 1 for town)
1648
+ const floor = idleState.phase === 'in_dungeon' && idleState.dungeonRunner
1649
+ ? idleState.dungeonRunner.getState()?.currentLevel ?? 1
1650
+ : 1;
1651
+ const floorNPCs = this.npcManager.getByFloor(floor);
1652
+ if (floorNPCs.length === 0) {
1653
+ this.ui.addEventLog('No NPCs nearby.');
1654
+ }
1655
+ else {
1656
+ this.ui.addEventLog('No NPCs in this room, but others are on this floor.');
1657
+ }
1658
+ this.npcSelectionMode = false;
1659
+ return;
1660
+ }
1661
+ // Enter NPC selection mode
1662
+ this.npcSelectionMode = true;
1663
+ this.availableNPCs = npcs;
1664
+ this.ui.addEventLog('─── NPCs Here ───');
1665
+ npcs.forEach((npc, i) => {
1666
+ const relationship = npc.playerRelationship > 0 ? '♥' : npc.playerRelationship < 0 ? '✗' : '○';
1667
+ const met = npc.metPlayer ? '' : ' (new)';
1668
+ this.ui.addEventLog(` [${i + 1}] ${npc.name} (${npc.role}) ${relationship}${met}`);
1669
+ });
1670
+ this.ui.addEventLog('Type a number to talk, or any command to cancel.');
1671
+ }
1672
+ /**
1673
+ * Handle NPC selection (when in NPC selection mode)
1674
+ */
1675
+ handleNPCSelection(num) {
1676
+ const index = num - 1;
1677
+ if (index >= 0 && index < this.availableNPCs.length) {
1678
+ const npc = this.availableNPCs[index];
1679
+ this.npcSelectionMode = false;
1680
+ // Use DialogHandler for NPC dialog
1681
+ this.dialogHandler.startDialog(npc);
1682
+ }
1683
+ else {
1684
+ this.ui.addEventLog(`Invalid selection. Choose 1-${this.availableNPCs.length}`);
1685
+ }
1686
+ }
1687
+ /**
1688
+ * Start dialog with an NPC
1689
+ */
1690
+ async startNPCDialog(npc) {
1691
+ this.currentDialogNPC = npc;
1692
+ this.dialogHistory = [];
1693
+ // Get greeting based on relationship
1694
+ const greeting = this.generateNPCGreeting(npc);
1695
+ this.dialogHistory.push({ role: 'npc', text: greeting });
1696
+ // Mark player as met
1697
+ npc.metPlayer = true;
1698
+ // Switch to dialog mode via router
1699
+ this.router.navigate({ type: 'dialog', npcId: npc.id });
1700
+ // Pause dungeon if in dungeon
1701
+ this.idleGameManager.pauseDungeon();
1702
+ // Set up dialog input handler
1703
+ this.ui.onDialogInputHandler((input) => this.handleDialogInput(input));
1704
+ // Render initial dialog screen
1705
+ this.renderDialogMode();
1706
+ }
1707
+ /**
1708
+ * Generate NPC greeting based on relationship and history
1709
+ */
1710
+ generateNPCGreeting(npc) {
1711
+ const relationship = npc.playerRelationship;
1712
+ const met = npc.dialogHistory.length > 0;
1713
+ if (!met) {
1714
+ // First meeting
1715
+ const greetings = {
1716
+ merchant: `Welcome, traveler! I am ${npc.name}. Looking to trade?`,
1717
+ sage: `Ah, a seeker of knowledge. I am ${npc.name}. What wisdom do you seek?`,
1718
+ guard: `Halt. I am ${npc.name}, keeper of this area. State your business.`,
1719
+ blacksmith: `*looks up from anvil* ${npc.name} here. Need something forged?`,
1720
+ traveler: `Well met, fellow wanderer. I'm ${npc.name}. Strange place, isn't it?`,
1721
+ quest_giver: `${npc.name} at your service. I might have work for someone like you.`,
1722
+ };
1723
+ return greetings[npc.role] || `Greetings. I am ${npc.name}.`;
1724
+ }
1725
+ else if (relationship > 50) {
1726
+ return `${npc.name} smiles warmly. "Good to see you again, friend!"`;
1727
+ }
1728
+ else if (relationship > 0) {
1729
+ return `"Ah, you're back," says ${npc.name}. "What can I do for you?"`;
1730
+ }
1731
+ else if (relationship < -50) {
1732
+ return `${npc.name} glares at you. "What do you want now?"`;
1733
+ }
1734
+ else if (relationship < 0) {
1735
+ return `${npc.name} regards you coldly. "Yes?"`;
1736
+ }
1737
+ else {
1738
+ return `${npc.name} nods in greeting. "How can I help?"`;
1739
+ }
1740
+ }
1741
+ /**
1742
+ * Handle dialog input
1743
+ */
1744
+ async handleDialogInput(input) {
1745
+ if (!this.currentDialogNPC)
1746
+ return;
1747
+ const trimmed = input.trim();
1748
+ const lower = trimmed.toLowerCase();
1749
+ // Check for exit commands
1750
+ if (lower === 'bye' || lower === 'exit' || trimmed === '') {
1751
+ this.endDialog();
1752
+ return;
1753
+ }
1754
+ // Check if awaiting quest response
1755
+ if (this.awaitingQuestResponse && this.pendingQuest) {
1756
+ if (lower === 'yes' || lower === 'y' || lower === 'accept') {
1757
+ this.acceptPendingQuest();
1758
+ return;
1759
+ }
1760
+ else if (lower === 'no' || lower === 'n' || lower === 'decline') {
1761
+ this.declinePendingQuest();
1762
+ return;
1763
+ }
1764
+ }
1765
+ // Check for quick options
1766
+ if (trimmed === '1') {
1767
+ this.handleQuickOption('quests');
1768
+ return;
1769
+ }
1770
+ else if (trimmed === '2') {
1771
+ this.handleQuickOption('trade');
1772
+ return;
1773
+ }
1774
+ else if (trimmed === '3') {
1775
+ this.handleQuickOption('lore');
1776
+ return;
1777
+ }
1778
+ // Add player message to history
1779
+ this.dialogHistory.push({ role: 'player', text: trimmed });
1780
+ this.renderDialogMode();
1781
+ // Get AI response
1782
+ const aiClient = getAIClient();
1783
+ const npc = this.currentDialogNPC;
1784
+ // Build context for AI
1785
+ const context = this.buildDialogContext(npc, trimmed);
1786
+ // Show thinking indicator
1787
+ this.ui.renderDialogScreen(npc, this.dialogHistory, this.getQuickOptions(), true);
1788
+ // Get response from AI (with built-in retry)
1789
+ const result = await aiClient.talkToNPC(npc.role, context);
1790
+ if (result.success && result.data) {
1791
+ // Reset failure counter on success
1792
+ this.consecutiveAIFailures = 0;
1793
+ // Add NPC response to history
1794
+ this.dialogHistory.push({ role: 'npc', text: result.data });
1795
+ // Record interaction in NPC manager
1796
+ this.npcManager.recordInteraction(npc.id, trimmed, result.data);
1797
+ // Check for relationship changes based on keywords
1798
+ this.updateRelationshipFromDialog(npc, trimmed, result.data);
1799
+ }
1800
+ else {
1801
+ // AI failed after retries
1802
+ this.consecutiveAIFailures++;
1803
+ // Report dialog failure with context
1804
+ console.error('[Dialog] AI failed:', result.error);
1805
+ Sentry.captureMessage('Dialog AI failed, using fallback response', {
1806
+ level: 'warning',
1807
+ tags: { operation: 'npc_dialog', fallback: true },
1808
+ extra: {
1809
+ npcId: npc.id,
1810
+ npcRole: npc.role,
1811
+ consecutiveFailures: this.consecutiveAIFailures,
1812
+ error: result.error,
1813
+ },
1814
+ });
1815
+ // Use fallback response
1816
+ const fallback = this.getFallbackResponse(this.currentDialogNPC);
1817
+ this.dialogHistory.push({ role: 'npc', text: fallback });
1818
+ // Check if we've hit too many consecutive failures
1819
+ if (result.retriesExhausted && this.consecutiveAIFailures >= this.MAX_CONSECUTIVE_FAILURES) {
1820
+ await this.promptLoadLastSave();
1821
+ }
1822
+ }
1823
+ this.renderDialogMode();
1824
+ }
1825
+ /**
1826
+ * Build context for AI dialog
1827
+ */
1828
+ buildDialogContext(npc, playerMessage) {
1829
+ const recentHistory = this.dialogHistory.slice(-6);
1830
+ const historyText = recentHistory
1831
+ .map(h => `${h.role === 'player' ? 'Player' : npc.name}: ${h.text}`)
1832
+ .join('\n');
1833
+ return `${historyText}\nPlayer: ${playerMessage}`;
1834
+ }
1835
+ /**
1836
+ * Update relationship based on dialog
1837
+ */
1838
+ updateRelationshipFromDialog(npc, playerMessage, npcResponse) {
1839
+ const lower = playerMessage.toLowerCase();
1840
+ // Positive keywords
1841
+ if (lower.includes('thank') || lower.includes('please') || lower.includes('help')) {
1842
+ this.npcManager.updateRelationship(npc.id, 1);
1843
+ }
1844
+ // Negative keywords
1845
+ if (lower.includes('stupid') || lower.includes('idiot') || lower.includes('hate')) {
1846
+ this.npcManager.updateRelationship(npc.id, -5);
1847
+ }
1848
+ }
1849
+ /**
1850
+ * Handle quick dialog options
1851
+ */
1852
+ async handleQuickOption(option) {
1853
+ if (!this.currentDialogNPC)
1854
+ return;
1855
+ const npc = this.currentDialogNPC;
1856
+ let playerText = '';
1857
+ let npcResponse = '';
1858
+ switch (option) {
1859
+ case 'quests':
1860
+ playerText = 'Do you have any tasks for me?';
1861
+ // Check if NPC has available quests
1862
+ const availableQuest = this.getQuestFromNPC(npc);
1863
+ if (availableQuest) {
1864
+ npcResponse = `Yes, I have a task. ${availableQuest.description} Will you accept? (Type 'yes' or 'no')`;
1865
+ // Store pending quest for acceptance
1866
+ this.pendingQuest = availableQuest;
1867
+ this.awaitingQuestResponse = true;
1868
+ }
1869
+ else {
1870
+ npcResponse = "I don't have anything for you right now. Check back later.";
1871
+ }
1872
+ break;
1873
+ case 'trade':
1874
+ playerText = 'What do you have for trade?';
1875
+ if (npc.role === 'merchant' || npc.role === 'blacksmith') {
1876
+ npcResponse = "Take a look at my wares. (Trade system coming soon)";
1877
+ }
1878
+ else {
1879
+ npcResponse = "I'm not a merchant, but perhaps someone else can help you.";
1880
+ }
1881
+ break;
1882
+ case 'lore':
1883
+ playerText = 'Tell me about this place.';
1884
+ npcResponse = this.getLocationLore(npc);
1885
+ break;
1886
+ }
1887
+ this.dialogHistory.push({ role: 'player', text: playerText });
1888
+ this.dialogHistory.push({ role: 'npc', text: npcResponse });
1889
+ this.npcManager.recordInteraction(npc.id, playerText, npcResponse);
1890
+ this.renderDialogMode();
1891
+ }
1892
+ pendingQuest = null;
1893
+ awaitingQuestResponse = false;
1894
+ /**
1895
+ * Accept a pending quest
1896
+ */
1897
+ acceptPendingQuest() {
1898
+ if (!this.pendingQuest || !this.currentDialogNPC)
1899
+ return;
1900
+ const npc = this.currentDialogNPC;
1901
+ const quest = this.pendingQuest;
1902
+ // Add to quest manager
1903
+ const success = this.questManager.acceptQuest(quest);
1904
+ if (success) {
1905
+ // Update NPC tracking
1906
+ if (!npc.givenQuests)
1907
+ npc.givenQuests = [];
1908
+ npc.givenQuests.push(quest.id);
1909
+ // Update relationship
1910
+ this.npcManager.updateRelationship(npc.id, 5);
1911
+ // Dialog response
1912
+ const responses = [
1913
+ `"Excellent! I knew I could count on you," says ${npc.name}.`,
1914
+ `${npc.name} nods approvingly. "Good luck out there."`,
1915
+ `"May the gods watch over you," ${npc.name} says with a smile.`,
1916
+ ];
1917
+ const response = responses[Math.floor(Math.random() * responses.length)];
1918
+ this.dialogHistory.push({ role: 'player', text: 'I accept.' });
1919
+ this.dialogHistory.push({ role: 'npc', text: response });
1920
+ this.ui.addEventLog(`✓ Quest accepted: ${quest.name}`);
1921
+ }
1922
+ else {
1923
+ this.dialogHistory.push({ role: 'npc', text: `"It seems you already have too many tasks. Come back when you're ready."` });
1924
+ }
1925
+ this.pendingQuest = null;
1926
+ this.awaitingQuestResponse = false;
1927
+ this.renderDialogMode();
1928
+ }
1929
+ /**
1930
+ * Decline a pending quest
1931
+ */
1932
+ declinePendingQuest() {
1933
+ if (!this.pendingQuest || !this.currentDialogNPC)
1934
+ return;
1935
+ const npc = this.currentDialogNPC;
1936
+ // Slight relationship decrease
1937
+ this.npcManager.updateRelationship(npc.id, -2);
1938
+ const responses = [
1939
+ `${npc.name} looks disappointed. "I understand. Perhaps another time."`,
1940
+ `"Very well," ${npc.name} sighs. "The offer stands if you change your mind."`,
1941
+ `${npc.name} shrugs. "Your choice. Others may be willing."`,
1942
+ ];
1943
+ const response = responses[Math.floor(Math.random() * responses.length)];
1944
+ this.dialogHistory.push({ role: 'player', text: 'I must decline.' });
1945
+ this.dialogHistory.push({ role: 'npc', text: response });
1946
+ this.pendingQuest = null;
1947
+ this.awaitingQuestResponse = false;
1948
+ this.renderDialogMode();
1949
+ }
1950
+ /**
1951
+ * Get quest from NPC if available
1952
+ */
1953
+ getQuestFromNPC(npc) {
1954
+ // Generate a simple quest based on NPC role
1955
+ const baseQuest = {
1956
+ giverNPCId: npc.id,
1957
+ giverNPCName: npc.name,
1958
+ giveDialog: `Will you help me with this task?`,
1959
+ progressDialog: `How is the task going?`,
1960
+ completeDialog: `Excellent work! Here is your reward.`,
1961
+ failDialog: `A shame you couldn't complete it.`,
1962
+ difficulty: 'medium',
1963
+ repeatable: false,
1964
+ timesCompleted: 0,
1965
+ };
1966
+ const questTemplates = {
1967
+ guard: () => ({
1968
+ ...baseQuest,
1969
+ id: `quest_${Date.now()}`,
1970
+ name: 'Clear the Threat',
1971
+ description: 'Eliminate the monsters lurking in the nearby rooms.',
1972
+ type: 'kill',
1973
+ status: 'available',
1974
+ objectives: [{
1975
+ id: 'kill_monsters',
1976
+ type: 'kill',
1977
+ target: 'enemy_*',
1978
+ targetName: 'enemies',
1979
+ description: 'Defeat 3 enemies',
1980
+ required: 3,
1981
+ current: 0,
1982
+ isComplete: false,
1983
+ }],
1984
+ rewards: [
1985
+ { type: 'exp', value: 50 },
1986
+ { type: 'gold', value: 25 },
1987
+ ],
1988
+ timeLimit: 30 * 60, // 30 minutes in seconds
1989
+ }),
1990
+ sage: () => ({
1991
+ ...baseQuest,
1992
+ id: `quest_${Date.now()}`,
1993
+ name: 'Seek Knowledge',
1994
+ description: 'Explore the dungeon and discover hidden secrets.',
1995
+ type: 'explore',
1996
+ status: 'available',
1997
+ objectives: [{
1998
+ id: 'explore_rooms',
1999
+ type: 'reach',
2000
+ target: 'room_*',
2001
+ targetName: 'rooms',
2002
+ description: 'Explore 5 rooms',
2003
+ required: 5,
2004
+ current: 0,
2005
+ isComplete: false,
2006
+ }],
2007
+ rewards: [
2008
+ { type: 'exp', value: 75 },
2009
+ { type: 'gold', value: 10 },
2010
+ ],
2011
+ }),
2012
+ merchant: () => ({
2013
+ ...baseQuest,
2014
+ id: `quest_${Date.now()}`,
2015
+ name: 'Trade Route',
2016
+ description: 'Help me retrieve some valuable goods.',
2017
+ type: 'collect',
2018
+ status: 'available',
2019
+ objectives: [{
2020
+ id: 'collect_items',
2021
+ type: 'collect',
2022
+ target: 'treasure_*',
2023
+ targetName: 'treasure',
2024
+ description: 'Find treasure',
2025
+ required: 1,
2026
+ current: 0,
2027
+ isComplete: false,
2028
+ }],
2029
+ rewards: [
2030
+ { type: 'exp', value: 30 },
2031
+ { type: 'gold', value: 50 },
2032
+ ],
2033
+ }),
2034
+ };
2035
+ const template = questTemplates[npc.role];
2036
+ if (template) {
2037
+ return template();
2038
+ }
2039
+ return null;
2040
+ }
2041
+ /**
2042
+ * Get location lore from NPC
2043
+ */
2044
+ getLocationLore(npc) {
2045
+ const loreOptions = [
2046
+ "This dungeon has stood for centuries. Many have entered, few have left.",
2047
+ "They say a great evil lurks in the depths below. Be careful if you venture there.",
2048
+ "The walls whisper secrets to those who listen. Ancient magic permeates this place.",
2049
+ "Long ago, this was a temple. Now it's home to creatures of darkness.",
2050
+ "I've heard rumors of powerful artifacts hidden in the boss rooms.",
2051
+ ];
2052
+ return loreOptions[Math.floor(Math.random() * loreOptions.length)];
2053
+ }
2054
+ /**
2055
+ * Get fallback response when AI fails
2056
+ */
2057
+ getFallbackResponse(npc) {
2058
+ const fallbacks = [
2059
+ `${npc.name} seems distracted. "Hmm? Sorry, I was thinking about something else."`,
2060
+ `"Interesting..." ${npc.name} strokes their chin thoughtfully.`,
2061
+ `${npc.name} nods slowly. "I see what you mean."`,
2062
+ `"That's... quite something," ${npc.name} responds carefully.`,
2063
+ ];
2064
+ return fallbacks[Math.floor(Math.random() * fallbacks.length)];
2065
+ }
2066
+ /**
2067
+ * Prompt user to load last save after multiple AI failures
2068
+ */
2069
+ async promptLoadLastSave() {
2070
+ // SENTRY ADD HERE - Report critical AI failure requiring save load prompt
2071
+ this.ui.addEventLog('');
2072
+ this.ui.addEventLog('═══════════════════════════════════════');
2073
+ this.ui.addEventLog('⚠ AI connection issues detected.');
2074
+ this.ui.addEventLog('Multiple attempts have failed.');
2075
+ this.ui.addEventLog('');
2076
+ this.ui.addEventLog('Options:');
2077
+ this.ui.addEventLog(' [R] Retry connection');
2078
+ this.ui.addEventLog(' [L] Load last save');
2079
+ this.ui.addEventLog('═══════════════════════════════════════');
2080
+ // Wait for valid input
2081
+ while (true) {
2082
+ this.input.setMode('raw');
2083
+ const key = await this.input.waitForKey();
2084
+ const keyName = key.name.toLowerCase();
2085
+ if (keyName === 'r') {
2086
+ // Retry - reset counter and let user try again
2087
+ this.consecutiveAIFailures = 0;
2088
+ this.ui.addEventLog('Retrying connection...');
2089
+ break;
2090
+ }
2091
+ else if (keyName === 'l') {
2092
+ // Reset failure counter
2093
+ this.consecutiveAIFailures = 0;
2094
+ // End current dialog if active
2095
+ if (this.currentDialogNPC) {
2096
+ this.endDialog();
2097
+ }
2098
+ // Trigger load
2099
+ await this.handleLoadCommand();
2100
+ break;
2101
+ }
2102
+ else {
2103
+ // Invalid key, wait for valid input
2104
+ this.ui.addEventLog('Press [R] to retry or [L] to load save.');
2105
+ }
2106
+ }
2107
+ }
2108
+ /**
2109
+ * Get quick dialog options
2110
+ */
2111
+ getQuickOptions() {
2112
+ return [
2113
+ { key: '1', text: 'Ask about quests', action: 'quests' },
2114
+ { key: '2', text: 'Trade', action: 'trade' },
2115
+ { key: '3', text: 'Ask about this place', action: 'lore' },
2116
+ ];
2117
+ }
2118
+ /**
2119
+ * Render dialog mode
2120
+ */
2121
+ renderDialogMode() {
2122
+ if (!this.currentDialogNPC)
2123
+ return;
2124
+ this.ui.renderDialogScreen(this.currentDialogNPC, this.dialogHistory, this.getQuickOptions(), false);
2125
+ }
2126
+ /**
2127
+ * End dialog with NPC
2128
+ */
2129
+ endDialog() {
2130
+ if (this.currentDialogNPC) {
2131
+ const farewell = this.generateFarewell(this.currentDialogNPC);
2132
+ this.ui.addEventLog(farewell);
2133
+ }
2134
+ this.currentDialogNPC = null;
2135
+ this.dialogHistory = [];
2136
+ this.pendingQuest = null;
2137
+ // Navigate back or to explore mode
2138
+ if (!this.router.back()) {
2139
+ this.router.navigate({ type: 'explore' });
2140
+ }
2141
+ // Resume dungeon if paused
2142
+ this.idleGameManager.resumeDungeon();
2143
+ }
2144
+ /**
2145
+ * Generate farewell message
2146
+ */
2147
+ generateFarewell(npc) {
2148
+ const relationship = npc.playerRelationship;
2149
+ if (relationship > 30) {
2150
+ return `${npc.name}: "Take care, friend. Come back anytime!"`;
2151
+ }
2152
+ else if (relationship < -30) {
2153
+ return `${npc.name} turns away without a word.`;
2154
+ }
2155
+ else {
2156
+ return `${npc.name}: "Farewell, traveler."`;
2157
+ }
2158
+ }
2159
+ /**
2160
+ * Handle quest command
2161
+ */
2162
+ handleQuestCommand() {
2163
+ const quests = this.questManager.getActiveQuests();
2164
+ if (quests.length === 0) {
2165
+ this.ui.addEventLog('No active quests. Talk to NPCs to get quests.');
2166
+ return;
2167
+ }
2168
+ this.ui.addEventLog('─── Active Quests ───');
2169
+ for (const quest of quests) {
2170
+ const completed = quest.objectives.filter(o => o.isComplete).length;
2171
+ const total = quest.objectives.length;
2172
+ this.ui.addEventLog(`• ${quest.name} [${completed}/${total}]`);
2173
+ }
2174
+ }
2175
+ /**
2176
+ * Show help
2177
+ */
2178
+ showHelp() {
2179
+ this.ui.addEventLog('─── Navigation ───');
2180
+ this.ui.addEventLog('Arrow keys: move between sections');
2181
+ this.ui.addEventLog('Enter: enter section | Esc: exit section');
2182
+ this.ui.addEventLog('─── Shortcuts ───');
2183
+ this.ui.addEventLog('[I]nv [C]har [Q]uest [W]ork [N]PC [H]elp');
2184
+ this.ui.addEventLog('[S]ave [L]oad | 1-9: quick travel');
2185
+ }
2186
+ /**
2187
+ * Handle save command
2188
+ */
2189
+ async handleSaveCommand() {
2190
+ const idleState = this.idleGameManager.getState();
2191
+ if (!idleState) {
2192
+ this.ui.addEventLog('✗ Failed to save game.');
2193
+ return;
2194
+ }
2195
+ // Create save data from IdleGameState
2196
+ const saveData = {
2197
+ version: '0.1.0',
2198
+ timestamp: Date.now(),
2199
+ player: idleState.player,
2200
+ currentMapId: idleState.currentTown.id,
2201
+ currentRoomId: `${idleState.currentTown.id}_center`,
2202
+ discoveredMaps: [idleState.currentTown.id],
2203
+ gameTime: idleState.statistics.totalPlayTime,
2204
+ settings: {
2205
+ language: 'en',
2206
+ autoSave: true,
2207
+ autoSaveInterval: 300,
2208
+ combatSpeed: 1,
2209
+ soundEnabled: false,
2210
+ },
2211
+ };
2212
+ // Also save NPC and quest state
2213
+ const npcSave = this.npcManager.save();
2214
+ const questSave = this.questManager.save();
2215
+ // Show save slots
2216
+ this.ui.addEventLog('─── Save Game ───');
2217
+ const slots = await listSaveSlots();
2218
+ for (const slot of slots) {
2219
+ if (slot.exists) {
2220
+ this.ui.addEventLog(` [${slot.slot}] ${slot.playerName} Lv.${slot.level} - ${formatTimestamp(slot.timestamp)}`);
2221
+ }
2222
+ else {
2223
+ this.ui.addEventLog(` [${slot.slot}] Empty`);
2224
+ }
2225
+ }
2226
+ this.ui.addEventLog('Type slot number (1-5) to save:');
2227
+ // Set up temporary command handler for slot selection
2228
+ this.pendingSaveData = { game: saveData, npcs: npcSave, quests: questSave };
2229
+ this.saveMode = true;
2230
+ }
2231
+ pendingSaveData = null;
2232
+ saveMode = false;
2233
+ loadMode = false;
2234
+ workshopMode = false;
2235
+ workshopSubMode = 'main';
2236
+ /**
2237
+ * Complete save to slot
2238
+ */
2239
+ async completeSave(slot) {
2240
+ if (!this.pendingSaveData)
2241
+ return;
2242
+ const result = await saveGame(slot, this.pendingSaveData.game, this.pendingSaveData.npcs, this.pendingSaveData.quests);
2243
+ if (result.success) {
2244
+ this.ui.addEventLog(`✓ Saved to slot ${slot}!`);
2245
+ this.ui.addEventLog(` Time played: ${formatPlayTime(this.pendingSaveData.game.gameTime)}`);
2246
+ }
2247
+ else {
2248
+ this.ui.addEventLog(`✗ Save failed: ${result.error}`);
2249
+ }
2250
+ this.pendingSaveData = null;
2251
+ this.saveMode = false;
2252
+ }
2253
+ /**
2254
+ * Handle load command
2255
+ */
2256
+ async handleLoadCommand() {
2257
+ this.ui.addEventLog('─── Load Game ───');
2258
+ const slots = await listSaveSlots();
2259
+ let hasAnySave = false;
2260
+ for (const slot of slots) {
2261
+ if (slot.exists) {
2262
+ hasAnySave = true;
2263
+ this.ui.addEventLog(` [${slot.slot}] ${slot.playerName} Lv.${slot.level} - ${formatPlayTime(slot.gameTime)}`);
2264
+ }
2265
+ else {
2266
+ this.ui.addEventLog(` [${slot.slot}] Empty`);
2267
+ }
2268
+ }
2269
+ if (!hasAnySave) {
2270
+ this.ui.addEventLog('No saved games found.');
2271
+ return;
2272
+ }
2273
+ this.ui.addEventLog('Type slot number (1-5) to load:');
2274
+ this.loadMode = true;
2275
+ }
2276
+ /**
2277
+ * Complete load from slot
2278
+ */
2279
+ async completeLoad(slot) {
2280
+ const result = await loadGame(slot);
2281
+ if (result.success && result.data) {
2282
+ // Destroy current game state and reinitialize with loaded player
2283
+ this.idleGameManager.destroy();
2284
+ this.idleGameManager.initialize(result.data.game.player);
2285
+ // Load NPC and quest state
2286
+ this.npcManager.load(result.data.npcs);
2287
+ this.questManager.load(result.data.quests);
2288
+ this.ui.addEventLog(`✓ Loaded from slot ${slot}!`);
2289
+ this.ui.addEventLog(` Welcome back, ${result.data.game.player.name}!`);
2290
+ }
2291
+ else {
2292
+ this.ui.addEventLog(`✗ Load failed: ${result.error}`);
2293
+ }
2294
+ this.loadMode = false;
2295
+ }
2296
+ /**
2297
+ * Handle new game command - opens character creation wizard
2298
+ * Uses NewGameFlowManager for consistent flow handling
2299
+ */
2300
+ handleNewGameCommand() {
2301
+ // Run the async flow without blocking
2302
+ this.runNewGameFlowFromMenu().catch((error) => {
2303
+ console.error('New game flow error:', error);
2304
+ // Safety fallback (try/finally in runNewGameFlowFromMenu should handle this)
2305
+ this.ui.resumeRendering();
2306
+ this.isInFullScreenMode = false;
2307
+ this.ui.addEventLog('New game cancelled.');
2308
+ });
2309
+ }
2310
+ /**
2311
+ * Run the New Game flow from the in-game menu
2312
+ * This destroys the current game and starts fresh
2313
+ */
2314
+ async runNewGameFlowFromMenu() {
2315
+ this.isInFullScreenMode = true;
2316
+ this.ui.pauseRendering(); // Stop GameUIAdapter refresh to prevent screen flicker
2317
+ try {
2318
+ const flowManager = new NewGameFlowManager(this.screen, this.input);
2319
+ const result = await flowManager.run();
2320
+ if (result.success && result.actor) {
2321
+ // Create team and reinitialize game
2322
+ const team = createTeamWithActor(result.actor, `${result.actor.name}'s Team`);
2323
+ const actors = { [result.actor.id]: result.actor };
2324
+ // Destroy old game and reinitialize
2325
+ this.idleGameManager.destroy();
2326
+ this.idleGameManager.initializeWithTeam(team, actors);
2327
+ // Resume normal game
2328
+ this.screen.clear();
2329
+ this.ui.addEventLog(`══════════════════════════════════════`);
2330
+ this.ui.addEventLog(` Welcome, ${result.actor.name} the ${result.actor.role}!`);
2331
+ this.ui.addEventLog(` Your adventure begins...`);
2332
+ this.ui.addEventLog(`══════════════════════════════════════`);
2333
+ }
2334
+ else {
2335
+ // Cancelled - return to current game
2336
+ this.ui.addEventLog('New game cancelled.');
2337
+ }
2338
+ }
2339
+ finally {
2340
+ this.ui.resumeRendering(); // Always restore GameUIAdapter refresh
2341
+ this.isInFullScreenMode = false;
2342
+ }
2343
+ }
2344
+ // =========================================================================
2345
+ // AFFIX WORKSHOP
2346
+ // =========================================================================
2347
+ /**
2348
+ * Handle workshop command
2349
+ */
2350
+ handleWorkshopCommand() {
2351
+ this.workshopMode = true;
2352
+ this.workshopSubMode = 'main';
2353
+ this.showWorkshopMenu();
2354
+ }
2355
+ /**
2356
+ * Show workshop main menu
2357
+ */
2358
+ showWorkshopMenu() {
2359
+ const player = this.idleGameManager.getState()?.player;
2360
+ if (!player)
2361
+ return;
2362
+ const unlockedCount = player.unlockedAffixes.length;
2363
+ const registry = getAffixRegistry();
2364
+ const totalAffixes = registry.getAll().length;
2365
+ this.ui.addEventLog('════════ AFFIX WORKSHOP ════════');
2366
+ this.ui.addEventLog(`Affixes discovered: ${unlockedCount}/${totalAffixes}`);
2367
+ this.ui.addEventLog('');
2368
+ this.ui.addEventLog('[1] View Affixes - Browse your collection');
2369
+ this.ui.addEventLog('[2] Forge - Apply affixes to weapons');
2370
+ this.ui.addEventLog('[3] Research - Study unknown affixes');
2371
+ this.ui.addEventLog('[0] Exit Workshop');
2372
+ this.ui.addEventLog('');
2373
+ this.ui.addEventLog('Select an option:');
2374
+ }
2375
+ /**
2376
+ * Handle workshop input
2377
+ */
2378
+ handleWorkshopInput(num) {
2379
+ if (this.workshopSubMode === 'main') {
2380
+ switch (num) {
2381
+ case 0:
2382
+ this.exitWorkshop();
2383
+ break;
2384
+ case 1:
2385
+ this.showAffixCollection();
2386
+ break;
2387
+ case 2:
2388
+ this.showForgeMenu();
2389
+ break;
2390
+ case 3:
2391
+ this.showResearchMenu();
2392
+ break;
2393
+ default:
2394
+ this.ui.addEventLog('Invalid option.');
2395
+ }
2396
+ }
2397
+ else if (this.workshopSubMode === 'view') {
2398
+ this.handleViewAffixInput(num);
2399
+ }
2400
+ else if (this.workshopSubMode === 'forge') {
2401
+ this.handleForgeInput(num);
2402
+ }
2403
+ else if (this.workshopSubMode === 'research') {
2404
+ this.handleResearchInput(num);
2405
+ }
2406
+ }
2407
+ /**
2408
+ * Show affix collection
2409
+ */
2410
+ showAffixCollection() {
2411
+ const player = this.idleGameManager.getState()?.player;
2412
+ if (!player)
2413
+ return;
2414
+ this.workshopSubMode = 'view';
2415
+ const registry = getAffixRegistry();
2416
+ const allAffixes = registry.getAll();
2417
+ this.ui.addEventLog('────── Your Affixes ──────');
2418
+ // Group by type
2419
+ const prefixes = allAffixes.filter(a => a.type === 'prefix');
2420
+ const suffixes = allAffixes.filter(a => a.type === 'suffix');
2421
+ const legendaries = allAffixes.filter(a => a.type === 'legendary');
2422
+ const isUnlocked = (id) => player.unlockedAffixes.includes(id);
2423
+ const rarityColor = (r) => {
2424
+ const colors = {
2425
+ common: '◯', uncommon: '●', rare: '◆', epic: '★', legendary: '✦'
2426
+ };
2427
+ return colors[r] || '?';
2428
+ };
2429
+ this.ui.addEventLog('');
2430
+ this.ui.addEventLog('PREFIXES:');
2431
+ prefixes.slice(0, 4).forEach((a, i) => {
2432
+ const status = isUnlocked(a.id) ? `${rarityColor(a.rarity)} ${a.name}` : `? ???`;
2433
+ this.ui.addEventLog(` [${i + 1}] ${status}`);
2434
+ });
2435
+ this.ui.addEventLog('');
2436
+ this.ui.addEventLog('SUFFIXES:');
2437
+ suffixes.slice(0, 4).forEach((a, i) => {
2438
+ const status = isUnlocked(a.id) ? `${rarityColor(a.rarity)} ${a.name}` : `? ???`;
2439
+ this.ui.addEventLog(` [${i + 5}] ${status}`);
2440
+ });
2441
+ this.ui.addEventLog('');
2442
+ this.ui.addEventLog('LEGENDARY:');
2443
+ legendaries.slice(0, 3).forEach((a, i) => {
2444
+ const status = isUnlocked(a.id) ? `✦ ${a.name}` : `? ???`;
2445
+ this.ui.addEventLog(` [${i + 9}] ${status}`);
2446
+ });
2447
+ this.ui.addEventLog('');
2448
+ this.ui.addEventLog('[0] Back to Workshop');
2449
+ }
2450
+ /**
2451
+ * Handle view affix input
2452
+ */
2453
+ handleViewAffixInput(num) {
2454
+ if (num === 0) {
2455
+ this.workshopSubMode = 'main';
2456
+ this.showWorkshopMenu();
2457
+ return;
2458
+ }
2459
+ const player = this.idleGameManager.getState()?.player;
2460
+ if (!player)
2461
+ return;
2462
+ const registry = getAffixRegistry();
2463
+ const allAffixes = registry.getAll();
2464
+ const prefixes = allAffixes.filter(a => a.type === 'prefix');
2465
+ const suffixes = allAffixes.filter(a => a.type === 'suffix');
2466
+ const legendaries = allAffixes.filter(a => a.type === 'legendary');
2467
+ let affix;
2468
+ if (num >= 1 && num <= 4) {
2469
+ affix = prefixes[num - 1];
2470
+ }
2471
+ else if (num >= 5 && num <= 8) {
2472
+ affix = suffixes[num - 5];
2473
+ }
2474
+ else if (num >= 9 && num <= 11) {
2475
+ affix = legendaries[num - 9];
2476
+ }
2477
+ if (affix && player.unlockedAffixes.includes(affix.id)) {
2478
+ this.showAffixDetails(affix);
2479
+ }
2480
+ else if (affix) {
2481
+ this.ui.addEventLog('This affix has not been discovered yet.');
2482
+ this.ui.addEventLog('Defeat enemies or complete quests to discover new affixes.');
2483
+ }
2484
+ else {
2485
+ this.ui.addEventLog('Invalid selection.');
2486
+ }
2487
+ }
2488
+ /**
2489
+ * Show affix details
2490
+ */
2491
+ showAffixDetails(affix) {
2492
+ const rarityLabels = {
2493
+ common: 'Common', uncommon: 'Uncommon', rare: 'Rare', epic: 'Epic', legendary: 'Legendary'
2494
+ };
2495
+ this.ui.addEventLog(`══════ ${affix.name} ══════`);
2496
+ this.ui.addEventLog(`Type: ${affix.type.toUpperCase()}`);
2497
+ this.ui.addEventLog(`Rarity: ${rarityLabels[affix.rarity]}`);
2498
+ this.ui.addEventLog('');
2499
+ this.ui.addEventLog(affix.description);
2500
+ if (affix.statModifiers) {
2501
+ this.ui.addEventLog('');
2502
+ this.ui.addEventLog('Stats:');
2503
+ for (const [stat, value] of Object.entries(affix.statModifiers)) {
2504
+ if (value !== undefined && value !== 0) {
2505
+ const sign = value > 0 ? '+' : '';
2506
+ this.ui.addEventLog(` ${stat}: ${sign}${value}`);
2507
+ }
2508
+ }
2509
+ }
2510
+ if (affix.onHitEffects && affix.onHitEffects.length > 0) {
2511
+ this.ui.addEventLog('');
2512
+ this.ui.addEventLog('On Hit:');
2513
+ for (const effect of affix.onHitEffects) {
2514
+ const chance = effect.chance < 1 ? `${Math.round(effect.chance * 100)}% chance` : '';
2515
+ this.ui.addEventLog(` ${effect.type} ${effect.element || ''} ${effect.value} ${chance}`);
2516
+ }
2517
+ }
2518
+ if (affix.requiresLevel) {
2519
+ this.ui.addEventLog('');
2520
+ this.ui.addEventLog(`Requires Level: ${affix.requiresLevel}`);
2521
+ }
2522
+ this.ui.addEventLog('');
2523
+ this.ui.addEventLog('[0] Back');
2524
+ }
2525
+ /**
2526
+ * Show forge menu
2527
+ */
2528
+ showForgeMenu() {
2529
+ const player = this.idleGameManager.getState()?.player;
2530
+ if (!player)
2531
+ return;
2532
+ this.workshopSubMode = 'forge';
2533
+ this.ui.addEventLog('────── Forge ──────');
2534
+ if (!player.equipment.weapon) {
2535
+ this.ui.addEventLog('You have no weapon equipped!');
2536
+ this.ui.addEventLog('Equip a weapon first to apply affixes.');
2537
+ }
2538
+ else {
2539
+ const weapon = player.equipment.weapon;
2540
+ this.ui.addEventLog(`Current Weapon: ${weapon.name}`);
2541
+ this.ui.addEventLog(`Attack: ${weapon.baseAttack} | Durability: ${weapon.durability}/${weapon.maxDurability}`);
2542
+ this.ui.addEventLog(`Affixes: ${weapon.affixes.length > 0 ? weapon.affixes.map(a => a.name).join(', ') : 'None'}`);
2543
+ this.ui.addEventLog('');
2544
+ if (player.unlockedAffixes.length === 0) {
2545
+ this.ui.addEventLog('You have no unlocked affixes to apply.');
2546
+ }
2547
+ else {
2548
+ this.ui.addEventLog('Available affixes to apply:');
2549
+ const registry = getAffixRegistry();
2550
+ player.unlockedAffixes.slice(0, 5).forEach((id, i) => {
2551
+ const affix = registry.get(id);
2552
+ if (affix) {
2553
+ this.ui.addEventLog(` [${i + 1}] ${affix.name} (${affix.rarity})`);
2554
+ }
2555
+ });
2556
+ this.ui.addEventLog('');
2557
+ this.ui.addEventLog('(Forging costs gold based on rarity)');
2558
+ }
2559
+ }
2560
+ this.ui.addEventLog('');
2561
+ this.ui.addEventLog('[0] Back to Workshop');
2562
+ }
2563
+ /**
2564
+ * Handle forge input
2565
+ */
2566
+ handleForgeInput(num) {
2567
+ if (num === 0) {
2568
+ this.workshopSubMode = 'main';
2569
+ this.showWorkshopMenu();
2570
+ return;
2571
+ }
2572
+ const player = this.idleGameManager.getState()?.player;
2573
+ if (!player || !player.equipment.weapon) {
2574
+ this.ui.addEventLog('No weapon to forge!');
2575
+ return;
2576
+ }
2577
+ const affixId = player.unlockedAffixes[num - 1];
2578
+ if (!affixId) {
2579
+ this.ui.addEventLog('Invalid selection.');
2580
+ return;
2581
+ }
2582
+ const registry = getAffixRegistry();
2583
+ const affix = registry.get(affixId);
2584
+ if (!affix) {
2585
+ this.ui.addEventLog('Affix not found.');
2586
+ return;
2587
+ }
2588
+ // Calculate cost
2589
+ const costByRarity = {
2590
+ common: 20, uncommon: 50, rare: 100, epic: 250, legendary: 500
2591
+ };
2592
+ const cost = costByRarity[affix.rarity] || 50;
2593
+ if (player.stats.gold < cost) {
2594
+ this.ui.addEventLog(`Not enough gold! Need ${cost}g, have ${player.stats.gold}g.`);
2595
+ return;
2596
+ }
2597
+ // Check if affix is compatible
2598
+ const existingAffixes = player.equipment.weapon.affixes;
2599
+ const hasConflict = existingAffixes.some(existing => affix.incompatibleWith?.includes(existing.definitionId) ||
2600
+ registry.get(existing.definitionId)?.incompatibleWith?.includes(affix.id));
2601
+ if (hasConflict) {
2602
+ this.ui.addEventLog('This affix conflicts with an existing one!');
2603
+ return;
2604
+ }
2605
+ // Apply affix
2606
+ player.stats.gold -= cost;
2607
+ player.equipment.weapon.affixes.push({
2608
+ definitionId: affix.id,
2609
+ name: affix.name,
2610
+ type: affix.type,
2611
+ rarity: affix.rarity,
2612
+ });
2613
+ this.ui.addEventLog(`✓ Applied "${affix.name}" to ${player.equipment.weapon.name}!`);
2614
+ this.ui.addEventLog(` Cost: ${cost}g | Remaining: ${player.stats.gold}g`);
2615
+ this.showForgeMenu();
2616
+ }
2617
+ /**
2618
+ * Show research menu
2619
+ */
2620
+ showResearchMenu() {
2621
+ const player = this.idleGameManager.getState()?.player;
2622
+ if (!player)
2623
+ return;
2624
+ this.workshopSubMode = 'research';
2625
+ const registry = getAffixRegistry();
2626
+ const allAffixes = registry.getAll();
2627
+ const locked = allAffixes.filter(a => !player.unlockedAffixes.includes(a.id));
2628
+ this.ui.addEventLog('────── Research ──────');
2629
+ this.ui.addEventLog('Study mysterious affixes to unlock them.');
2630
+ this.ui.addEventLog('');
2631
+ if (locked.length === 0) {
2632
+ this.ui.addEventLog('All affixes have been discovered!');
2633
+ }
2634
+ else {
2635
+ this.ui.addEventLog(`Unknown affixes: ${locked.length}`);
2636
+ this.ui.addEventLog('');
2637
+ // Show hints for a few random locked affixes
2638
+ const hints = locked.slice(0, 3);
2639
+ hints.forEach((a, i) => {
2640
+ const hint = this.getAffixHint(a);
2641
+ this.ui.addEventLog(` [${i + 1}] ${hint} (${a.rarity})`);
2642
+ });
2643
+ this.ui.addEventLog('');
2644
+ this.ui.addEventLog('Research cost: 100g per attempt (50% success)');
2645
+ }
2646
+ this.ui.addEventLog('');
2647
+ this.ui.addEventLog('[0] Back to Workshop');
2648
+ }
2649
+ /**
2650
+ * Get a hint for an unknown affix
2651
+ */
2652
+ getAffixHint(affix) {
2653
+ // Provide vague hints based on type/effects
2654
+ if (affix.type === 'legendary') {
2655
+ return '??? (Legendary power)';
2656
+ }
2657
+ if (affix.onHitEffects?.some(e => e.element === 'fire')) {
2658
+ return '??? (Burns with flames)';
2659
+ }
2660
+ if (affix.onHitEffects?.some(e => e.element === 'ice')) {
2661
+ return '??? (Chills to the bone)';
2662
+ }
2663
+ if (affix.onHitEffects?.some(e => e.element === 'lightning')) {
2664
+ return '??? (Crackles with energy)';
2665
+ }
2666
+ if (affix.statModifiers?.attack && affix.statModifiers.attack > 10) {
2667
+ return '??? (Raw power)';
2668
+ }
2669
+ if (affix.statModifiers?.speed && affix.statModifiers.speed > 0) {
2670
+ return '??? (Swift as wind)';
2671
+ }
2672
+ return '??? (Unknown properties)';
2673
+ }
2674
+ /**
2675
+ * Handle research input
2676
+ */
2677
+ handleResearchInput(num) {
2678
+ if (num === 0) {
2679
+ this.workshopSubMode = 'main';
2680
+ this.showWorkshopMenu();
2681
+ return;
2682
+ }
2683
+ const player = this.idleGameManager.getState()?.player;
2684
+ if (!player)
2685
+ return;
2686
+ const registry = getAffixRegistry();
2687
+ const allAffixes = registry.getAll();
2688
+ const locked = allAffixes.filter(a => !player.unlockedAffixes.includes(a.id));
2689
+ const targetAffix = locked[num - 1];
2690
+ if (!targetAffix) {
2691
+ this.ui.addEventLog('Invalid selection.');
2692
+ return;
2693
+ }
2694
+ const cost = 100;
2695
+ if (player.stats.gold < cost) {
2696
+ this.ui.addEventLog(`Not enough gold! Need ${cost}g, have ${player.stats.gold}g.`);
2697
+ return;
2698
+ }
2699
+ player.stats.gold -= cost;
2700
+ // 50% success chance (increased by higher rarity being harder)
2701
+ const rarityBonus = {
2702
+ common: 0.3, uncommon: 0.2, rare: 0.1, epic: 0, legendary: -0.2
2703
+ };
2704
+ const successChance = 0.5 + (rarityBonus[targetAffix.rarity] || 0);
2705
+ const success = Math.random() < successChance;
2706
+ if (success) {
2707
+ player.unlockedAffixes.push(targetAffix.id);
2708
+ this.ui.addEventLog(`✓ Research successful!`);
2709
+ this.ui.addEventLog(` Discovered: ${targetAffix.name}`);
2710
+ this.ui.addEventLog(` "${targetAffix.description}"`);
2711
+ }
2712
+ else {
2713
+ this.ui.addEventLog('✗ Research failed...');
2714
+ this.ui.addEventLog(' The affix remains mysterious.');
2715
+ this.ui.addEventLog(' Try again or seek clues in the dungeon.');
2716
+ }
2717
+ this.ui.addEventLog(` Gold spent: ${cost}g | Remaining: ${player.stats.gold}g`);
2718
+ }
2719
+ /**
2720
+ * Exit workshop
2721
+ */
2722
+ exitWorkshop() {
2723
+ this.workshopMode = false;
2724
+ this.workshopSubMode = 'main';
2725
+ this.ui.addEventLog('Leaving the Workshop...');
2726
+ }
2727
+ // =========================================================================
2728
+ // EVENT HANDLERS
2729
+ // =========================================================================
2730
+ onTick() {
2731
+ // Tick handling done in main loop
2732
+ }
2733
+ onTravelStarted(data) {
2734
+ const travel = data;
2735
+ this.ui.addEventLog(`Setting out... (${travel.duration}s)`);
2736
+ // Route change handled via onIdlePhaseChange callback
2737
+ }
2738
+ onTravelArrived(data) {
2739
+ const { room } = data;
2740
+ this.ui.addEventLog(`Arrived at: ${room.name}`);
2741
+ this.ui.addEventLog(room.description);
2742
+ this.router.navigate({ type: 'explore' });
2743
+ // Check for random NPC encounter
2744
+ if (Math.random() < 0.3) {
2745
+ const idleState = this.idleGameManager.getState();
2746
+ if (idleState) {
2747
+ const floor = idleState.phase === 'in_dungeon' && idleState.dungeonRunner
2748
+ ? idleState.dungeonRunner.getState()?.currentLevel ?? 1
2749
+ : 1;
2750
+ const npc = getNPCGenerator().generate({ floor });
2751
+ this.ui.addEventLog(`You encounter ${npc.name}, a ${npc.race} ${npc.role}.`);
2752
+ }
2753
+ }
2754
+ }
2755
+ onCombatStarted(data) {
2756
+ this.ui.addEventLog('== Combat started! ==');
2757
+ this.router.navigate({ type: 'combat', encounterId: 'current' });
2758
+ }
2759
+ onCombatTick(data) {
2760
+ // Handled by render
2761
+ }
2762
+ onCombatVictory(data) {
2763
+ const { rewards } = data;
2764
+ this.ui.addEventLog('Victory!');
2765
+ this.ui.addEventLog(`Gained ${rewards.exp} EXP, ${rewards.gold} gold`);
2766
+ this.router.navigate({ type: 'explore' });
2767
+ // Update quest progress - use adapted state for combat info
2768
+ const idleState = this.idleGameManager.getState();
2769
+ if (idleState?.dungeonRunner) {
2770
+ const runnerState = idleState.dungeonRunner.getState();
2771
+ if (runnerState?.activeCombat) {
2772
+ for (const enemy of runnerState.activeCombat.enemies) {
2773
+ this.questManager.updateProgress('kill', enemy.id);
2774
+ this.questManager.updateProgress('kill', `enemy_${enemy.name.toLowerCase().replace(/\s/g, '_')}`);
2775
+ }
2776
+ }
2777
+ }
2778
+ }
2779
+ onCombatDefeat() {
2780
+ this.ui.addEventLog('You have been defeated...');
2781
+ }
2782
+ onPlayerRespawn(data) {
2783
+ const { room } = data;
2784
+ this.ui.addEventLog('═══ RESPAWNING ═══');
2785
+ this.ui.addEventLog(`You wake up at ${room.name}, fully healed.`);
2786
+ this.ui.addEventLog('Your adventure continues...');
2787
+ this.router.navigate({ type: 'explore' });
2788
+ }
2789
+ onNewEvent(data) {
2790
+ const event = data;
2791
+ this.ui.addEventLog(`${event.title}: ${event.description}`);
2792
+ }
2793
+ // =========================================================================
2794
+ // UTILITIES
2795
+ // =========================================================================
2796
+ sleep(ms) {
2797
+ return new Promise(resolve => setTimeout(resolve, ms));
2798
+ }
2799
+ cleanup() {
2800
+ this.isRunning = false;
2801
+ this.idleGameManager.destroy();
2802
+ this.ui.cleanup();
2803
+ // Exit alternate screen buffer to restore terminal history
2804
+ this.screen.exitAlternateScreen();
2805
+ }
2806
+ }
2807
+ // ============================================================================
2808
+ // ENTRY POINT
2809
+ // ============================================================================
2810
+ const app = new IdleRPGTerminal();
2811
+ app.start().catch((error) => {
2812
+ console.error('Fatal error:', error);
2813
+ process.exit(1);
2814
+ });
2815
+ //# sourceMappingURL=main.js.map
2816
+ //# debugId=bfa08172-04b0-56af-9640-104baeb3ae89