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
@@ -0,0 +1,2116 @@
1
+ /**
2
+ * Game UI - Main game interface with multiple modes
3
+ * Now uses section-based architecture for unified interactions
4
+ */
5
+
6
+ !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]="7d089040-391e-5d75-84d0-9dd7c0b0e3ea")}catch(e){}}();
7
+ import { getScreen, ANSI, MAP_SYMBOLS } from './Screen.js';
8
+ import { getInput } from './Input.js';
9
+ import { getStrings } from '../i18n/index.js';
10
+ // Section-based architecture
11
+ import { SectionManager, createGridLayout } from './core/SectionManager.js';
12
+ import { MapSection } from './sections/MapSection.js';
13
+ import { StatusSection } from './sections/StatusSection.js';
14
+ import { ActionsSection } from './sections/ActionsSection.js';
15
+ import { EventsSection } from './sections/EventsSection.js';
16
+ /**
17
+ * Main Game UI class
18
+ */
19
+ export class GameUI {
20
+ // Global padding for entire UI
21
+ PADDING = 2;
22
+ screen;
23
+ input;
24
+ mode = 'explore';
25
+ refreshInterval = null;
26
+ eventLog = [];
27
+ maxLogLines = 8;
28
+ inputBuffer = '';
29
+ inputPrompt = '> ';
30
+ // Dialog state
31
+ dialogNPC = null;
32
+ dialogOptions = [];
33
+ dialogHistory = [];
34
+ isWaitingForAI = false;
35
+ // Combat state cache
36
+ combatTurnLog = [];
37
+ // Callbacks
38
+ onCommand = null;
39
+ onDialogInput = null;
40
+ // Modal state
41
+ activeModal = null;
42
+ modalSelectedIndex = 0;
43
+ modalScrollOffset = 0;
44
+ modalActiveTab = 0;
45
+ // Section navigation state (legacy - kept for compatibility)
46
+ // Sections: 'map' (top-left), 'status' (mid-left), 'actions' (right), 'events' (bottom)
47
+ focusedSection = 'actions';
48
+ inSection = false;
49
+ sectionSelectedIndex = 0;
50
+ sectionScrollOffset = 0;
51
+ // New section-based architecture
52
+ sectionManager;
53
+ mapSection;
54
+ statusSection;
55
+ actionsSection;
56
+ eventsSection;
57
+ useSectionArchitecture = true; // Enable new architecture
58
+ constructor() {
59
+ this.screen = getScreen();
60
+ this.input = getInput();
61
+ // Initialize section-based architecture
62
+ this.sectionManager = new SectionManager();
63
+ this.mapSection = new MapSection();
64
+ this.statusSection = new StatusSection();
65
+ this.actionsSection = new ActionsSection();
66
+ this.eventsSection = new EventsSection();
67
+ // Register sections
68
+ this.sectionManager.register(this.mapSection);
69
+ this.sectionManager.register(this.statusSection);
70
+ this.sectionManager.register(this.actionsSection);
71
+ this.sectionManager.register(this.eventsSection);
72
+ // Set up navigation layout
73
+ // +-------+--------+
74
+ // | map | actions|
75
+ // +-------+ |
76
+ // | status| |
77
+ // +-------+--------+
78
+ // | events |
79
+ // +----------------+
80
+ const layout = createGridLayout([
81
+ ['map', 'actions'],
82
+ ['status', 'actions'],
83
+ ['events', 'events'],
84
+ ]);
85
+ this.sectionManager.setLayout(layout);
86
+ this.sectionManager.setDefaultFocus('actions');
87
+ // Set up section callbacks
88
+ this.setupSectionCallbacks();
89
+ }
90
+ /**
91
+ * Set up callbacks for section interactions
92
+ */
93
+ setupSectionCallbacks() {
94
+ // Map travel callback
95
+ this.mapSection.setOnTravel((roomIndex) => {
96
+ if (this.onCommand) {
97
+ this.onCommand(String(roomIndex + 1));
98
+ }
99
+ });
100
+ // Actions NPC select callback
101
+ this.actionsSection.setOnNpcSelect((npcIndex) => {
102
+ if (this.onCommand) {
103
+ this.onCommand('npc');
104
+ setTimeout(() => this.onCommand?.(String(npcIndex + 1)), 50);
105
+ }
106
+ });
107
+ // Actions modal callback
108
+ this.actionsSection.setOnOpenModal((modal) => {
109
+ this.openModal(modal);
110
+ });
111
+ }
112
+ // Size warning state
113
+ showingSizeWarning = false;
114
+ /**
115
+ * Initialize UI
116
+ */
117
+ async initialize() {
118
+ this.screen.hideCursor();
119
+ this.input.start();
120
+ // Set up key handling
121
+ this.input.onKey((key) => this.handleKeypress(key));
122
+ this.input.onLine((line) => this.handleLine(line));
123
+ // Set up resize handling
124
+ this.screen.onResize((width, height) => this.handleResize(width, height));
125
+ // Initialize section manager focus
126
+ if (this.useSectionArchitecture) {
127
+ this.sectionManager.focusDefault();
128
+ }
129
+ // Start in explore mode
130
+ this.setMode('explore');
131
+ }
132
+ /**
133
+ * Handle terminal resize
134
+ */
135
+ handleResize(width, height) {
136
+ const sizeCheck = this.screen.checkMinimumSize();
137
+ if (!sizeCheck.valid) {
138
+ // Terminal too small - show warning
139
+ this.showingSizeWarning = true;
140
+ this.screen.renderSizeWarning();
141
+ }
142
+ else {
143
+ // Size is OK - always redraw after resize
144
+ const wasShowingWarning = this.showingSizeWarning;
145
+ this.showingSizeWarning = false;
146
+ this.screen.clear();
147
+ // Force redraw based on current mode
148
+ if (this.mode === 'explore' && this.cachedState) {
149
+ this.renderMainScreen(this.cachedState, this.cachedNPCs, this.cachedQuests);
150
+ }
151
+ else if (this.mode === 'dialog' && this.dialogNPC) {
152
+ this.renderDialogScreen(this.dialogNPC, this.dialogHistory, this.dialogOptions, this.isWaitingForAI);
153
+ }
154
+ else if (this.mode === 'combat' || this.mode === 'travel') {
155
+ // Combat and travel modes will re-render on next tick
156
+ // Just force a clear for now
157
+ }
158
+ this.screen.forceRedraw();
159
+ }
160
+ }
161
+ /**
162
+ * Check if currently showing size warning
163
+ */
164
+ isShowingSizeWarning() {
165
+ return this.showingSizeWarning;
166
+ }
167
+ /**
168
+ * Cleanup
169
+ */
170
+ cleanup() {
171
+ this.stopRefresh();
172
+ this.input.stop();
173
+ this.screen.cleanup();
174
+ }
175
+ /**
176
+ * Set UI mode
177
+ */
178
+ setMode(mode) {
179
+ const prevMode = this.mode;
180
+ this.mode = mode;
181
+ // Reset menu selection on mode change
182
+ if (mode === 'explore') {
183
+ this.menuSelectedIndex = 0;
184
+ }
185
+ // Handle mode transitions
186
+ // Enable refresh for all modes to keep screen updated (clock, input display, etc.)
187
+ if (mode === 'travel' || mode === 'combat') {
188
+ this.startRefresh(100); // 10 FPS for smooth combat/travel
189
+ }
190
+ else if (mode === 'explore' || mode === 'dialog') {
191
+ this.startRefresh(200); // 5 FPS for explore/dialog (input feedback + clock)
192
+ }
193
+ else {
194
+ this.stopRefresh();
195
+ }
196
+ // Explore mode uses raw input for direct keyboard navigation
197
+ // Dialog mode uses line input for text entry
198
+ if (mode === 'dialog') {
199
+ this.input.setMode('line');
200
+ }
201
+ else {
202
+ this.input.setMode('raw');
203
+ }
204
+ }
205
+ // State cache for refresh
206
+ cachedState = null;
207
+ cachedNPCs = [];
208
+ cachedQuests = [];
209
+ /**
210
+ * Start screen refresh
211
+ */
212
+ startRefresh(intervalMs) {
213
+ this.stopRefresh();
214
+ this.refreshInterval = setInterval(() => {
215
+ // Skip refresh if showing size warning or resizing
216
+ if (this.showingSizeWarning || this.screen.isCurrentlyResizing()) {
217
+ return;
218
+ }
219
+ // Update input buffer from InputManager
220
+ this.inputBuffer = this.input.getInputBuffer();
221
+ // Render modal if active
222
+ if (this.activeModal) {
223
+ this.renderModal();
224
+ return;
225
+ }
226
+ // Re-render the current mode with cached data
227
+ if (this.mode === 'explore' && this.cachedState) {
228
+ this.renderMainScreen(this.cachedState, this.cachedNPCs, this.cachedQuests);
229
+ }
230
+ else if (this.mode === 'dialog' && this.dialogNPC) {
231
+ this.renderDialogScreen(this.dialogNPC, this.dialogHistory, this.dialogOptions, this.isWaitingForAI);
232
+ }
233
+ else {
234
+ this.screen.render();
235
+ }
236
+ }, intervalMs);
237
+ }
238
+ /**
239
+ * Stop screen refresh
240
+ */
241
+ stopRefresh() {
242
+ if (this.refreshInterval) {
243
+ clearInterval(this.refreshInterval);
244
+ this.refreshInterval = null;
245
+ }
246
+ }
247
+ /**
248
+ * Handle keypress
249
+ */
250
+ handleKeypress(key) {
251
+ // Global hotkeys
252
+ if (key.ctrl && key.name === 'c') {
253
+ this.cleanup();
254
+ process.exit(0);
255
+ }
256
+ // Handle modal input first
257
+ if (this.activeModal) {
258
+ this.handleModalKey(key);
259
+ return;
260
+ }
261
+ // Mode-specific handling
262
+ switch (this.mode) {
263
+ case 'combat':
264
+ this.handleCombatKey(key);
265
+ break;
266
+ case 'travel':
267
+ this.handleTravelKey(key);
268
+ break;
269
+ case 'menu':
270
+ this.handleMenuKey(key);
271
+ break;
272
+ case 'explore':
273
+ // Menu-based navigation in explore mode
274
+ this.handleExploreKey(key);
275
+ break;
276
+ case 'dialog':
277
+ // Handled by line mode
278
+ this.inputBuffer = this.input.getInputBuffer();
279
+ break;
280
+ }
281
+ }
282
+ /**
283
+ * Handle line input
284
+ */
285
+ handleLine(line) {
286
+ if (this.mode === 'explore' && this.onCommand) {
287
+ this.onCommand(line.trim().toLowerCase());
288
+ }
289
+ else if (this.mode === 'dialog' && this.onDialogInput) {
290
+ this.onDialogInput(line);
291
+ }
292
+ this.inputBuffer = '';
293
+ }
294
+ /**
295
+ * Handle explore mode keypress (section-based navigation)
296
+ */
297
+ handleExploreKey(key) {
298
+ // Modal shortcuts - always work regardless of section state
299
+ const modalKeys = {
300
+ 'i': 'inventory',
301
+ 'c': 'character',
302
+ 'q': 'quests',
303
+ 'w': 'workshop',
304
+ 'm': 'menu',
305
+ };
306
+ if (key.raw && modalKeys[key.raw.toLowerCase()]) {
307
+ this.openModal(modalKeys[key.raw.toLowerCase()]);
308
+ return;
309
+ }
310
+ // Quick number shortcuts for travel - always work
311
+ if (key.raw && /^[1-9]$/.test(key.raw) && this.onCommand) {
312
+ this.onCommand(key.raw);
313
+ return;
314
+ }
315
+ // Other global shortcuts (only when not in a section)
316
+ const globalShortcuts = {
317
+ 'n': 'npc',
318
+ 'h': 'help',
319
+ 's': 'save',
320
+ 'l': 'load',
321
+ };
322
+ // Use new section-based architecture for key handling
323
+ if (this.useSectionArchitecture) {
324
+ // Global shortcuts only work when not entered in a section
325
+ if (!this.sectionManager.isAnyEntered() && key.raw) {
326
+ const cmd = globalShortcuts[key.raw.toLowerCase()];
327
+ if (cmd && this.onCommand) {
328
+ this.onCommand(cmd);
329
+ return;
330
+ }
331
+ }
332
+ // Delegate to section manager
333
+ if (this.sectionManager.handleKey(key)) {
334
+ return;
335
+ }
336
+ return;
337
+ }
338
+ // Legacy handling (fallback)
339
+ if (key.raw) {
340
+ const cmd = globalShortcuts[key.raw.toLowerCase()];
341
+ if (cmd && this.onCommand) {
342
+ this.onCommand(cmd);
343
+ return;
344
+ }
345
+ }
346
+ // Section-based navigation (legacy)
347
+ if (this.inSection) {
348
+ this.handleInSectionKey(key);
349
+ }
350
+ else {
351
+ this.handleSectionNavigationKey(key);
352
+ }
353
+ }
354
+ /**
355
+ * Handle navigation between sections
356
+ */
357
+ handleSectionNavigationKey(key) {
358
+ // Section layout:
359
+ // +-------+--------+
360
+ // | map | actions|
361
+ // +-------+ |
362
+ // | status| |
363
+ // +-------+--------+
364
+ // | events |
365
+ // +----------------+
366
+ switch (key.name) {
367
+ case 'up':
368
+ case 'k':
369
+ if (this.focusedSection === 'events') {
370
+ this.focusedSection = 'status';
371
+ }
372
+ else if (this.focusedSection === 'status') {
373
+ this.focusedSection = 'map';
374
+ }
375
+ break;
376
+ case 'down':
377
+ case 'j':
378
+ if (this.focusedSection === 'map') {
379
+ this.focusedSection = 'status';
380
+ }
381
+ else if (this.focusedSection === 'status' || this.focusedSection === 'actions') {
382
+ this.focusedSection = 'events';
383
+ }
384
+ break;
385
+ case 'left':
386
+ case 'h':
387
+ if (this.focusedSection === 'actions') {
388
+ this.focusedSection = 'map';
389
+ }
390
+ break;
391
+ case 'right':
392
+ case 'l':
393
+ if (this.focusedSection === 'map' || this.focusedSection === 'status') {
394
+ this.focusedSection = 'actions';
395
+ }
396
+ break;
397
+ case 'return':
398
+ case 'space':
399
+ // Enter the focused section
400
+ this.inSection = true;
401
+ this.sectionSelectedIndex = 0;
402
+ this.sectionScrollOffset = 0;
403
+ break;
404
+ case 'escape':
405
+ // Show system hints
406
+ this.addEventLog('[S] Save [L] Load [H] Help');
407
+ break;
408
+ }
409
+ }
410
+ /**
411
+ * Handle navigation within a section
412
+ */
413
+ handleInSectionKey(key) {
414
+ switch (key.name) {
415
+ case 'escape':
416
+ // Exit the section
417
+ this.inSection = false;
418
+ break;
419
+ case 'up':
420
+ case 'k':
421
+ this.sectionSelectedIndex = Math.max(0, this.sectionSelectedIndex - 1);
422
+ this.ensureSectionItemVisible();
423
+ break;
424
+ case 'down':
425
+ case 'j':
426
+ this.sectionSelectedIndex++;
427
+ this.ensureSectionItemVisible();
428
+ break;
429
+ case 'return':
430
+ case 'space':
431
+ this.activateSectionItem();
432
+ break;
433
+ }
434
+ }
435
+ /**
436
+ * Ensure the selected item in a section is visible
437
+ */
438
+ ensureSectionItemVisible() {
439
+ const maxVisible = this.getSectionMaxVisible();
440
+ if (this.sectionSelectedIndex < this.sectionScrollOffset) {
441
+ this.sectionScrollOffset = this.sectionSelectedIndex;
442
+ }
443
+ else if (this.sectionSelectedIndex >= this.sectionScrollOffset + maxVisible) {
444
+ this.sectionScrollOffset = this.sectionSelectedIndex - maxVisible + 1;
445
+ }
446
+ }
447
+ /**
448
+ * Get max visible items for current section
449
+ */
450
+ getSectionMaxVisible() {
451
+ const { height } = this.screen.getSize();
452
+ switch (this.focusedSection) {
453
+ case 'map':
454
+ return 4; // Connected rooms
455
+ case 'actions':
456
+ return Math.max(1, Math.floor(height * 0.4) - 4);
457
+ case 'events':
458
+ return Math.max(1, Math.floor(height * 0.25) - 2);
459
+ default:
460
+ return 5;
461
+ }
462
+ }
463
+ /**
464
+ * Activate the selected item in current section
465
+ */
466
+ activateSectionItem() {
467
+ switch (this.focusedSection) {
468
+ case 'map':
469
+ // Travel to selected room
470
+ if (this.cachedState) {
471
+ const connections = this.cachedState.currentRoom.connections;
472
+ if (this.sectionSelectedIndex < connections.length && this.onCommand) {
473
+ this.onCommand(String(this.sectionSelectedIndex + 1));
474
+ this.inSection = false;
475
+ }
476
+ }
477
+ break;
478
+ case 'actions':
479
+ // Activate selected action
480
+ const item = this.menuItems[this.sectionSelectedIndex];
481
+ if (item) {
482
+ if (item.type === 'npc') {
483
+ this.onCommand?.('npc');
484
+ const npcIndex = parseInt(item.id.replace('npc_', ''), 10);
485
+ setTimeout(() => this.onCommand?.(String(npcIndex + 1)), 50);
486
+ }
487
+ else if (item.type === 'action') {
488
+ const modalActions = {
489
+ 'inventory': 'inventory',
490
+ 'character': 'character',
491
+ 'quests': 'quests',
492
+ 'workshop': 'workshop',
493
+ };
494
+ if (modalActions[item.id]) {
495
+ this.openModal(modalActions[item.id]);
496
+ }
497
+ }
498
+ this.inSection = false;
499
+ }
500
+ break;
501
+ case 'events':
502
+ // Events are read-only, just show a message
503
+ this.addEventLog('(Events are read-only)');
504
+ break;
505
+ }
506
+ }
507
+ /**
508
+ * Select the currently highlighted menu item
509
+ */
510
+ selectCurrentMenuItem() {
511
+ if (this.menuItems.length === 0)
512
+ return;
513
+ const item = this.menuItems[this.menuSelectedIndex];
514
+ if (!item)
515
+ return;
516
+ switch (item.type) {
517
+ case 'travel':
518
+ // Extract room index from id (e.g., 'travel_0' -> '1')
519
+ const travelIndex = parseInt(item.id.replace('travel_', ''), 10);
520
+ this.onCommand?.(String(travelIndex + 1));
521
+ break;
522
+ case 'npc':
523
+ // First enter NPC mode, then select
524
+ this.onCommand?.('npc');
525
+ // The npc id contains the index
526
+ const npcIndex = parseInt(item.id.replace('npc_', ''), 10);
527
+ setTimeout(() => this.onCommand?.(String(npcIndex + 1)), 50);
528
+ break;
529
+ case 'action':
530
+ // Open modal for inventory, character, quests, workshop
531
+ const modalActions = {
532
+ 'inventory': 'inventory',
533
+ 'character': 'character',
534
+ 'quests': 'quests',
535
+ 'workshop': 'workshop',
536
+ };
537
+ if (modalActions[item.id]) {
538
+ this.openModal(modalActions[item.id]);
539
+ }
540
+ break;
541
+ }
542
+ }
543
+ /**
544
+ * Open a modal screen
545
+ */
546
+ openModal(modal) {
547
+ this.activeModal = modal;
548
+ this.modalSelectedIndex = 0;
549
+ this.modalScrollOffset = 0;
550
+ this.modalActiveTab = 0;
551
+ this.renderModal();
552
+ }
553
+ /**
554
+ * Close the active modal
555
+ */
556
+ closeModal() {
557
+ this.activeModal = null;
558
+ // Force redraw of main screen
559
+ if (this.cachedState) {
560
+ this.renderMainScreen(this.cachedState, this.cachedNPCs, this.cachedQuests);
561
+ }
562
+ }
563
+ /**
564
+ * Handle modal key input
565
+ */
566
+ handleModalKey(key) {
567
+ // Character modal has special handling for editing
568
+ if (this.activeModal === 'character') {
569
+ // Only allow escape to close when not editing
570
+ if (key.name === 'escape' && !this.characterEditField) {
571
+ this.closeModal();
572
+ return;
573
+ }
574
+ // Close on 'c' key when not editing
575
+ if (key.raw?.toLowerCase() === 'c' && !this.characterEditField) {
576
+ this.closeModal();
577
+ return;
578
+ }
579
+ this.handleCharacterModalKey(key);
580
+ return;
581
+ }
582
+ // Close modal on Escape or same key
583
+ if (key.name === 'escape') {
584
+ this.closeModal();
585
+ return;
586
+ }
587
+ // Close on same key that opened it
588
+ const modalKeys = {
589
+ 'i': 'inventory',
590
+ 'c': 'character',
591
+ 'q': 'quests',
592
+ 'w': 'workshop',
593
+ 'm': 'menu',
594
+ };
595
+ if (key.raw && modalKeys[key.raw.toLowerCase()] === this.activeModal) {
596
+ this.closeModal();
597
+ return;
598
+ }
599
+ // Tab switching for inventory
600
+ if (this.activeModal === 'inventory' && key.raw && /^[1-5]$/.test(key.raw)) {
601
+ this.modalActiveTab = parseInt(key.raw) - 1;
602
+ this.modalSelectedIndex = 0;
603
+ this.modalScrollOffset = 0;
604
+ this.renderModal();
605
+ return;
606
+ }
607
+ // Menu modal special handling
608
+ if (this.activeModal === 'menu') {
609
+ this.handleMenuModalKey(key);
610
+ return;
611
+ }
612
+ // Navigation
613
+ switch (key.name) {
614
+ case 'up':
615
+ case 'k':
616
+ this.modalSelectedIndex = Math.max(0, this.modalSelectedIndex - 1);
617
+ this.renderModal();
618
+ break;
619
+ case 'down':
620
+ case 'j':
621
+ this.modalSelectedIndex++;
622
+ this.renderModal();
623
+ break;
624
+ case 'left':
625
+ case 'h':
626
+ if (this.modalActiveTab > 0) {
627
+ this.modalActiveTab--;
628
+ this.modalSelectedIndex = 0;
629
+ this.renderModal();
630
+ }
631
+ break;
632
+ case 'right':
633
+ case 'l':
634
+ this.modalActiveTab++;
635
+ this.modalSelectedIndex = 0;
636
+ this.renderModal();
637
+ break;
638
+ }
639
+ }
640
+ /**
641
+ * Handle menu modal key presses
642
+ */
643
+ handleMenuModalKey(key) {
644
+ const menuItems = ['save', 'load', 'quit'];
645
+ switch (key.name) {
646
+ case 'up':
647
+ case 'k':
648
+ this.modalSelectedIndex = Math.max(0, this.modalSelectedIndex - 1);
649
+ this.renderModal();
650
+ break;
651
+ case 'down':
652
+ case 'j':
653
+ this.modalSelectedIndex = Math.min(menuItems.length - 1, this.modalSelectedIndex + 1);
654
+ this.renderModal();
655
+ break;
656
+ case 'return':
657
+ const action = menuItems[this.modalSelectedIndex];
658
+ this.closeModal();
659
+ if (this.onCommand) {
660
+ this.onCommand(action);
661
+ }
662
+ break;
663
+ }
664
+ // Quick shortcuts within menu
665
+ if (key.raw) {
666
+ const shortcuts = {
667
+ 's': 'save',
668
+ 'l': 'load',
669
+ 'q': 'quit',
670
+ };
671
+ const cmd = shortcuts[key.raw.toLowerCase()];
672
+ if (cmd) {
673
+ this.closeModal();
674
+ if (this.onCommand) {
675
+ this.onCommand(cmd);
676
+ }
677
+ }
678
+ }
679
+ }
680
+ /**
681
+ * Render the active modal
682
+ */
683
+ renderModal() {
684
+ if (!this.activeModal || !this.cachedState)
685
+ return;
686
+ const { width: screenWidth, height: screenHeight } = this.screen.getSize();
687
+ this.screen.clear();
688
+ // Apply global padding
689
+ const px = this.PADDING;
690
+ const py = this.PADDING;
691
+ const width = screenWidth - this.PADDING * 2;
692
+ const height = screenHeight - this.PADDING * 2;
693
+ switch (this.activeModal) {
694
+ case 'inventory':
695
+ this.renderInventoryModal(px, py, width, height);
696
+ break;
697
+ case 'character':
698
+ this.renderCharacterModal(px, py, width, height);
699
+ break;
700
+ case 'quests':
701
+ this.renderQuestsModal(px, py, width, height);
702
+ break;
703
+ case 'workshop':
704
+ this.renderWorkshopModal(px, py, width, height);
705
+ break;
706
+ case 'menu':
707
+ this.renderMenuModal(px, py, width, height);
708
+ break;
709
+ }
710
+ this.screen.render();
711
+ }
712
+ /**
713
+ * Render inventory modal
714
+ */
715
+ renderInventoryModal(px, py, width, height) {
716
+ const player = this.cachedState?.player;
717
+ if (!player)
718
+ return;
719
+ // Full screen modal
720
+ this.screen.drawBox(px, py, width, height, 'Inventory', true);
721
+ this.screen.write(px + width - 15, py + 1, 'Esc/I to close', ANSI.fg.gray);
722
+ // Tabs
723
+ const tabs = ['All', 'Weapons', 'Armor', 'Items', 'Materials'];
724
+ let tabX = px + 2;
725
+ for (let i = 0; i < tabs.length; i++) {
726
+ const isActive = i === this.modalActiveTab;
727
+ const fg = isActive ? ANSI.fg.black : ANSI.fg.white;
728
+ const bg = isActive ? ANSI.bg.cyan : undefined;
729
+ this.screen.write(tabX, py + 2, ` [${i + 1}] ${tabs[i]} `, fg, bg);
730
+ tabX += tabs[i].length + 7;
731
+ }
732
+ // Items list
733
+ const items = player.inventory;
734
+ const listY = py + 4;
735
+ const listHeight = height - 8;
736
+ this.screen.drawBox(px + 1, listY, Math.floor(width * 0.5), listHeight, 'Items');
737
+ if (items.length === 0) {
738
+ this.screen.write(px + 3, listY + 2, 'No items in inventory', ANSI.fg.gray);
739
+ }
740
+ else {
741
+ const maxVisible = listHeight - 2;
742
+ for (let i = 0; i < Math.min(items.length, maxVisible); i++) {
743
+ const itemIndex = this.modalScrollOffset + i;
744
+ if (itemIndex >= items.length)
745
+ break;
746
+ const item = items[itemIndex];
747
+ const isSelected = itemIndex === this.modalSelectedIndex;
748
+ const fg = isSelected ? ANSI.fg.black : ANSI.fg.white;
749
+ const bg = isSelected ? ANSI.bg.cyan : undefined;
750
+ const indicator = isSelected ? '>' : ' ';
751
+ const qty = item.quantity > 1 ? ` x${item.quantity}` : '';
752
+ this.screen.write(px + 2, listY + 1 + i, indicator, ANSI.fg.yellow);
753
+ this.screen.write(px + 4, listY + 1 + i, `${item.name}${qty}`.padEnd(Math.floor(width * 0.5) - 6), fg, bg);
754
+ }
755
+ }
756
+ // Details panel
757
+ const detailX = px + Math.floor(width * 0.5) + 2;
758
+ const detailW = width - Math.floor(width * 0.5) - 3;
759
+ this.screen.drawBox(detailX, listY, detailW, listHeight, 'Details');
760
+ if (items.length > 0 && this.modalSelectedIndex < items.length) {
761
+ const item = items[this.modalSelectedIndex];
762
+ const itemData = item.data;
763
+ let y = listY + 1;
764
+ this.screen.write(detailX + 2, y++, item.name, ANSI.fg.yellow, undefined, ANSI.bold);
765
+ y++;
766
+ this.screen.write(detailX + 2, y++, `Type: ${item.type}`, ANSI.fg.gray);
767
+ if (itemData?.rarity) {
768
+ this.screen.write(detailX + 2, y++, `Rarity: ${itemData.rarity}`, ANSI.fg.cyan);
769
+ }
770
+ if (item.description) {
771
+ y++;
772
+ this.screen.write(detailX + 2, y++, item.description.slice(0, detailW - 4), ANSI.fg.white);
773
+ }
774
+ }
775
+ // Equipment summary
776
+ const eq = player.equipment;
777
+ const eqY = py + height - 3;
778
+ const wpn = eq.weapon ? eq.weapon.name : '(none)';
779
+ const arm = eq.armor ? eq.armor.name : '(none)';
780
+ this.screen.write(px + 2, eqY, `Equipped: Weapon: ${wpn} Armor: ${arm}`, ANSI.fg.gray);
781
+ // Hint
782
+ this.screen.write(px + 2, py + height - 2, '1-5:Tabs Up/Down:Select Esc:Close', ANSI.fg.gray);
783
+ }
784
+ // Character editing state
785
+ characterEditField = null;
786
+ characterEditBuffer = '';
787
+ onCharacterEdit = null;
788
+ /**
789
+ * Register character edit handler
790
+ */
791
+ onCharacterEditHandler(handler) {
792
+ this.onCharacterEdit = handler;
793
+ }
794
+ /**
795
+ * Render character modal with tabs for Stats and Customize
796
+ */
797
+ renderCharacterModal(px, py, width, height) {
798
+ const player = this.cachedState?.player;
799
+ if (!player)
800
+ return;
801
+ this.screen.drawBox(px, py, width, height, 'Character', true);
802
+ this.screen.write(px + width - 15, py + 1, 'Esc/C to close', ANSI.fg.gray);
803
+ // Tabs: Stats, Customize
804
+ const tabs = ['Stats', 'Customize'];
805
+ let tabX = px + 2;
806
+ for (let i = 0; i < tabs.length; i++) {
807
+ const isActive = i === this.modalActiveTab;
808
+ const fg = isActive ? ANSI.fg.black : ANSI.fg.white;
809
+ const bg = isActive ? ANSI.bg.cyan : undefined;
810
+ this.screen.write(tabX, py + 2, ` [${i + 1}] ${tabs[i]} `, fg, bg);
811
+ tabX += tabs[i].length + 7;
812
+ }
813
+ if (this.modalActiveTab === 0) {
814
+ this.renderCharacterStatsTab(px, py, width, height, player);
815
+ }
816
+ else {
817
+ this.renderCharacterCustomizeTab(px, py, width, height, player);
818
+ }
819
+ }
820
+ /**
821
+ * Render character stats tab
822
+ */
823
+ renderCharacterStatsTab(px, py, width, height, player) {
824
+ const s = player.stats;
825
+ let y = py + 4;
826
+ const centerX = px + Math.floor(width / 2) - 15;
827
+ // Name and level
828
+ this.screen.write(centerX, y++, player.name, ANSI.fg.yellow, undefined, ANSI.bold);
829
+ this.screen.write(centerX, y++, `Level ${s.level}`, ANSI.fg.cyan);
830
+ y++;
831
+ // Stats box
832
+ this.screen.drawBox(centerX - 2, y, 35, 12, 'Stats');
833
+ y++;
834
+ this.screen.write(centerX, y++, `HP: ${s.hp} / ${s.maxHp}`, ANSI.fg.red);
835
+ this.screen.write(centerX, y++, `MP: ${s.mp} / ${s.maxMp}`, ANSI.fg.blue);
836
+ y++;
837
+ this.screen.write(centerX, y++, `Attack: ${s.attack}`, ANSI.fg.white);
838
+ this.screen.write(centerX, y++, `Defense: ${s.defense}`, ANSI.fg.white);
839
+ this.screen.write(centerX, y++, `Speed: ${s.speed}`, ANSI.fg.white);
840
+ y++;
841
+ this.screen.write(centerX, y++, `EXP: ${s.exp} / ${s.expToNextLevel}`, ANSI.fg.magenta);
842
+ this.screen.write(centerX, y++, `Gold: ${s.gold}`, ANSI.fg.yellow);
843
+ // Equipment
844
+ y += 2;
845
+ this.screen.drawBox(centerX - 2, y, 35, 6, 'Equipment');
846
+ y++;
847
+ const eq = player.equipment;
848
+ this.screen.write(centerX, y++, `Weapon: ${eq.weapon?.name || '(none)'}`, ANSI.fg.white);
849
+ this.screen.write(centerX, y++, `Armor: ${eq.armor?.name || '(none)'}`, ANSI.fg.white);
850
+ this.screen.write(centerX, y++, `Access: ${eq.accessory?.name || '(none)'}`, ANSI.fg.white);
851
+ // Hint
852
+ this.screen.write(px + 2, py + height - 2, '1-2:Tabs Esc:Close', ANSI.fg.gray);
853
+ }
854
+ /**
855
+ * Render character customize tab
856
+ */
857
+ renderCharacterCustomizeTab(px, py, width, height, player) {
858
+ const char = player.character || {
859
+ backstory: '',
860
+ appearance: '',
861
+ personality: '',
862
+ motivation: '',
863
+ origin: '',
864
+ traits: [],
865
+ };
866
+ let y = py + 4;
867
+ const leftX = px + 3;
868
+ const fieldWidth = Math.min(50, width - 10);
869
+ // Editable fields
870
+ const fields = [
871
+ { key: 'name', label: 'Name', value: player.name },
872
+ { key: 'backstory', label: 'Background Story', value: char.backstory || '(not set)' },
873
+ { key: 'appearance', label: 'Appearance', value: char.appearance || '(not set)' },
874
+ { key: 'personality', label: 'Personality', value: char.personality || '(not set)' },
875
+ { key: 'motivation', label: 'Motivation', value: char.motivation || '(not set)' },
876
+ { key: 'origin', label: 'Origin', value: char.origin || '(not set)' },
877
+ ];
878
+ this.screen.write(leftX, y++, '─── Character Customization ───', ANSI.fg.cyan);
879
+ y++;
880
+ for (let i = 0; i < fields.length; i++) {
881
+ const field = fields[i];
882
+ const isSelected = this.modalSelectedIndex === i;
883
+ const isEditing = this.characterEditField === field.key;
884
+ const labelFg = isSelected ? ANSI.fg.yellow : ANSI.fg.gray;
885
+ const valueFg = isSelected ? ANSI.fg.white : ANSI.fg.gray;
886
+ const bg = isSelected ? ANSI.bg.blue : undefined;
887
+ const indicator = isSelected ? '>' : ' ';
888
+ this.screen.write(leftX, y, indicator, ANSI.fg.yellow);
889
+ this.screen.write(leftX + 2, y, `${field.label}:`, labelFg);
890
+ y++;
891
+ if (isEditing) {
892
+ // Show edit buffer with cursor
893
+ const displayValue = this.characterEditBuffer + '▌';
894
+ this.screen.write(leftX + 4, y, displayValue.slice(0, fieldWidth - 6).padEnd(fieldWidth - 6), ANSI.fg.brightCyan, ANSI.bg.gray);
895
+ }
896
+ else {
897
+ // Show current value (truncated)
898
+ const displayValue = field.value.slice(0, fieldWidth - 6);
899
+ this.screen.write(leftX + 4, y, displayValue.padEnd(fieldWidth - 6), valueFg, isSelected ? bg : undefined);
900
+ }
901
+ y += 2;
902
+ }
903
+ // Traits section (read-only for now, showing as comma-separated)
904
+ y++;
905
+ this.screen.write(leftX, y++, '─── Traits ───', ANSI.fg.cyan);
906
+ const traitsText = char.traits.length > 0 ? char.traits.join(', ') : '(no traits defined)';
907
+ this.screen.write(leftX + 2, y++, traitsText.slice(0, fieldWidth), ANSI.fg.gray);
908
+ // Hint at bottom
909
+ if (this.characterEditField) {
910
+ this.screen.write(px + 2, py + height - 2, 'Type to edit Enter:Save Esc:Cancel', ANSI.fg.gray);
911
+ }
912
+ else {
913
+ this.screen.write(px + 2, py + height - 2, 'Up/Down:Select Enter:Edit 1-2:Tabs Esc:Close', ANSI.fg.gray);
914
+ }
915
+ }
916
+ /**
917
+ * Handle character modal key presses (extended for customization)
918
+ */
919
+ handleCharacterModalKey(key) {
920
+ // If in edit mode, handle text input
921
+ if (this.characterEditField) {
922
+ if (key.name === 'escape') {
923
+ // Cancel edit
924
+ this.characterEditField = null;
925
+ this.characterEditBuffer = '';
926
+ this.renderModal();
927
+ return;
928
+ }
929
+ else if (key.name === 'return') {
930
+ // Save edit
931
+ if (this.onCharacterEdit && this.characterEditField && this.characterEditBuffer.trim()) {
932
+ this.onCharacterEdit(this.characterEditField, this.characterEditBuffer.trim());
933
+ }
934
+ this.characterEditField = null;
935
+ this.characterEditBuffer = '';
936
+ this.renderModal();
937
+ return;
938
+ }
939
+ else if (key.name === 'backspace') {
940
+ this.characterEditBuffer = this.characterEditBuffer.slice(0, -1);
941
+ this.renderModal();
942
+ return;
943
+ }
944
+ else if (key.raw && !key.ctrl && !key.meta) {
945
+ const charCode = key.raw.charCodeAt(0);
946
+ if (charCode >= 32 && this.characterEditBuffer.length < 200) {
947
+ this.characterEditBuffer += key.raw;
948
+ this.renderModal();
949
+ }
950
+ return;
951
+ }
952
+ return;
953
+ }
954
+ // Tab switching
955
+ if (key.raw && /^[1-2]$/.test(key.raw)) {
956
+ this.modalActiveTab = parseInt(key.raw) - 1;
957
+ this.modalSelectedIndex = 0;
958
+ this.renderModal();
959
+ return;
960
+ }
961
+ // Navigation in customize tab
962
+ if (this.modalActiveTab === 1) {
963
+ const maxIndex = 5; // 6 fields (0-5)
964
+ switch (key.name) {
965
+ case 'up':
966
+ case 'k':
967
+ this.modalSelectedIndex = Math.max(0, this.modalSelectedIndex - 1);
968
+ this.renderModal();
969
+ break;
970
+ case 'down':
971
+ case 'j':
972
+ this.modalSelectedIndex = Math.min(maxIndex, this.modalSelectedIndex + 1);
973
+ this.renderModal();
974
+ break;
975
+ case 'return':
976
+ // Start editing the selected field
977
+ const fields = ['name', 'backstory', 'appearance', 'personality', 'motivation', 'origin'];
978
+ this.characterEditField = fields[this.modalSelectedIndex];
979
+ // Pre-fill with current value
980
+ const player = this.cachedState?.player;
981
+ if (player) {
982
+ if (this.characterEditField === 'name') {
983
+ this.characterEditBuffer = player.name;
984
+ }
985
+ else if (player.character) {
986
+ this.characterEditBuffer = player.character[this.characterEditField] || '';
987
+ }
988
+ else {
989
+ this.characterEditBuffer = '';
990
+ }
991
+ }
992
+ this.renderModal();
993
+ break;
994
+ }
995
+ }
996
+ }
997
+ /**
998
+ * Render quests modal
999
+ */
1000
+ renderQuestsModal(px, py, width, height) {
1001
+ const quests = this.cachedQuests;
1002
+ this.screen.drawBox(px, py, width, height, 'Quests', true);
1003
+ this.screen.write(px + width - 15, py + 1, 'Esc/Q to close', ANSI.fg.gray);
1004
+ // Tabs: Active, Completed
1005
+ const tabs = ['Active', 'Completed'];
1006
+ let tabX = px + 2;
1007
+ for (let i = 0; i < tabs.length; i++) {
1008
+ const isActive = i === this.modalActiveTab;
1009
+ const fg = isActive ? ANSI.fg.black : ANSI.fg.white;
1010
+ const bg = isActive ? ANSI.bg.cyan : undefined;
1011
+ this.screen.write(tabX, py + 2, ` ${tabs[i]} `, fg, bg);
1012
+ tabX += tabs[i].length + 3;
1013
+ }
1014
+ const listY = py + 4;
1015
+ const listHeight = height - 6;
1016
+ // Filter quests based on tab
1017
+ const filteredQuests = this.modalActiveTab === 0
1018
+ ? quests.filter(q => q.status === 'active')
1019
+ : quests.filter(q => q.status === 'completed');
1020
+ if (filteredQuests.length === 0) {
1021
+ this.screen.write(px + 3, listY + 2, this.modalActiveTab === 0 ? 'No active quests' : 'No completed quests', ANSI.fg.gray);
1022
+ }
1023
+ else {
1024
+ // Quest list
1025
+ const listW = Math.floor(width * 0.4);
1026
+ this.screen.drawBox(px + 1, listY, listW, listHeight, 'Quest List');
1027
+ for (let i = 0; i < Math.min(filteredQuests.length, listHeight - 2); i++) {
1028
+ const quest = filteredQuests[i];
1029
+ const isSelected = i === this.modalSelectedIndex;
1030
+ const fg = isSelected ? ANSI.fg.black : ANSI.fg.white;
1031
+ const bg = isSelected ? ANSI.bg.cyan : undefined;
1032
+ const indicator = isSelected ? '>' : ' ';
1033
+ const completed = quest.objectives.filter(o => o.isComplete).length;
1034
+ const total = quest.objectives.length;
1035
+ this.screen.write(px + 2, listY + 1 + i, indicator, ANSI.fg.yellow);
1036
+ this.screen.write(px + 4, listY + 1 + i, `${quest.name} [${completed}/${total}]`.slice(0, listW - 6).padEnd(listW - 6), fg, bg);
1037
+ }
1038
+ // Details panel
1039
+ const detailX = px + listW + 2;
1040
+ const detailW = width - listW - 3;
1041
+ this.screen.drawBox(detailX, listY, detailW, listHeight, 'Details');
1042
+ if (this.modalSelectedIndex < filteredQuests.length) {
1043
+ const quest = filteredQuests[this.modalSelectedIndex];
1044
+ let y = listY + 1;
1045
+ this.screen.write(detailX + 2, y++, quest.name, ANSI.fg.yellow, undefined, ANSI.bold);
1046
+ y++;
1047
+ this.screen.write(detailX + 2, y++, quest.description.slice(0, detailW - 4), ANSI.fg.white);
1048
+ y++;
1049
+ this.screen.write(detailX + 2, y++, 'Objectives:', ANSI.fg.cyan);
1050
+ for (const obj of quest.objectives.slice(0, 5)) {
1051
+ const check = obj.isComplete ? '[x]' : '[ ]';
1052
+ this.screen.write(detailX + 2, y++, `${check} ${obj.description}`.slice(0, detailW - 4), obj.isComplete ? ANSI.fg.green : ANSI.fg.white);
1053
+ }
1054
+ }
1055
+ }
1056
+ // Hint
1057
+ this.screen.write(px + 2, py + height - 2, 'Left/Right:Tabs Up/Down:Select Esc:Close', ANSI.fg.gray);
1058
+ }
1059
+ /**
1060
+ * Render workshop modal
1061
+ */
1062
+ renderWorkshopModal(px, py, width, height) {
1063
+ const player = this.cachedState?.player;
1064
+ if (!player)
1065
+ return;
1066
+ this.screen.drawBox(px, py, width, height, 'Workshop', true);
1067
+ this.screen.write(px + width - 15, py + 1, 'Esc/W to close', ANSI.fg.gray);
1068
+ // Tabs
1069
+ const tabs = ['Affixes', 'Forge', 'Research'];
1070
+ let tabX = px + 2;
1071
+ for (let i = 0; i < tabs.length; i++) {
1072
+ const isActive = i === this.modalActiveTab;
1073
+ const fg = isActive ? ANSI.fg.black : ANSI.fg.white;
1074
+ const bg = isActive ? ANSI.bg.cyan : undefined;
1075
+ this.screen.write(tabX, py + 2, ` [${i + 1}] ${tabs[i]} `, fg, bg);
1076
+ tabX += tabs[i].length + 7;
1077
+ }
1078
+ const contentY = py + 4;
1079
+ const centerX = px + Math.floor(width / 2) - 20;
1080
+ if (this.modalActiveTab === 0) {
1081
+ // Affixes tab
1082
+ this.screen.write(centerX, contentY, 'Your Unlocked Affixes:', ANSI.fg.cyan);
1083
+ const affixes = player.unlockedAffixes || [];
1084
+ if (affixes.length === 0) {
1085
+ this.screen.write(centerX, contentY + 2, 'No affixes discovered yet.', ANSI.fg.gray);
1086
+ this.screen.write(centerX, contentY + 3, 'Defeat enemies or complete quests to discover affixes.', ANSI.fg.gray);
1087
+ }
1088
+ else {
1089
+ for (let i = 0; i < Math.min(affixes.length, 10); i++) {
1090
+ const isSelected = i === this.modalSelectedIndex;
1091
+ const fg = isSelected ? ANSI.fg.black : ANSI.fg.white;
1092
+ const bg = isSelected ? ANSI.bg.cyan : undefined;
1093
+ this.screen.write(centerX, contentY + 2 + i, affixes[i].padEnd(30), fg, bg);
1094
+ }
1095
+ }
1096
+ }
1097
+ else if (this.modalActiveTab === 1) {
1098
+ // Forge tab
1099
+ this.screen.write(centerX, contentY, 'Forge Affixes onto Weapons', ANSI.fg.cyan);
1100
+ if (!player.equipment.weapon) {
1101
+ this.screen.write(centerX, contentY + 2, 'No weapon equipped.', ANSI.fg.gray);
1102
+ this.screen.write(centerX, contentY + 3, 'Equip a weapon to apply affixes.', ANSI.fg.gray);
1103
+ }
1104
+ else {
1105
+ this.screen.write(centerX, contentY + 2, `Current Weapon: ${player.equipment.weapon.name}`, ANSI.fg.yellow);
1106
+ const affixCount = player.equipment.weapon.affixes?.length || 0;
1107
+ this.screen.write(centerX, contentY + 3, `Affixes: ${affixCount}`, ANSI.fg.white);
1108
+ }
1109
+ }
1110
+ else {
1111
+ // Research tab
1112
+ this.screen.write(centerX, contentY, 'Research Unknown Affixes', ANSI.fg.cyan);
1113
+ this.screen.write(centerX, contentY + 2, 'Cost: 100 gold per attempt', ANSI.fg.gray);
1114
+ this.screen.write(centerX, contentY + 3, 'Success chance: 50%', ANSI.fg.gray);
1115
+ this.screen.write(centerX, contentY + 5, `Your Gold: ${player.stats.gold}`, ANSI.fg.yellow);
1116
+ }
1117
+ // Hint
1118
+ this.screen.write(px + 2, py + height - 2, '1-3:Tabs Up/Down:Select Esc:Close', ANSI.fg.gray);
1119
+ }
1120
+ /**
1121
+ * Render menu modal (save/load/quit)
1122
+ */
1123
+ renderMenuModal(px, py, width, height) {
1124
+ // Smaller centered modal within the padded area
1125
+ const modalWidth = 40;
1126
+ const modalHeight = 14;
1127
+ const modalX = px + Math.floor((width - modalWidth) / 2);
1128
+ const modalY = py + Math.floor((height - modalHeight) / 2);
1129
+ // Draw modal box
1130
+ this.screen.drawBox(modalX, modalY, modalWidth, modalHeight, 'Game Menu', true);
1131
+ this.screen.write(modalX + modalWidth - 12, modalY + 1, 'Esc to close', ANSI.fg.gray);
1132
+ const menuItems = [
1133
+ { key: 'S', label: 'Save Game', desc: 'Save your current progress' },
1134
+ { key: 'L', label: 'Load Game', desc: 'Load a previous save' },
1135
+ { key: 'Q', label: 'Quit Game', desc: 'Exit to desktop' },
1136
+ ];
1137
+ const startY = modalY + 3;
1138
+ const centerX = modalX + 4;
1139
+ for (let i = 0; i < menuItems.length; i++) {
1140
+ const item = menuItems[i];
1141
+ const isSelected = i === this.modalSelectedIndex;
1142
+ const fg = isSelected ? ANSI.fg.black : ANSI.fg.white;
1143
+ const bg = isSelected ? ANSI.bg.cyan : undefined;
1144
+ const indicator = isSelected ? '>' : ' ';
1145
+ // Draw menu item
1146
+ this.screen.write(centerX, startY + i * 3, indicator, ANSI.fg.yellow);
1147
+ this.screen.write(centerX + 2, startY + i * 3, `[${item.key}] ${item.label}`.padEnd(modalWidth - 10), fg, bg);
1148
+ this.screen.write(centerX + 2, startY + i * 3 + 1, item.desc, ANSI.fg.gray);
1149
+ }
1150
+ // Hint at bottom
1151
+ this.screen.write(modalX + 2, modalY + modalHeight - 2, 'Up/Down:Select Enter:Confirm', ANSI.fg.gray);
1152
+ }
1153
+ /**
1154
+ * Handle combat keypress
1155
+ */
1156
+ handleCombatKey(key) {
1157
+ switch (key.name) {
1158
+ case '1':
1159
+ this.addEventLog('Strategy: Aggressive');
1160
+ break;
1161
+ case '2':
1162
+ this.addEventLog('Strategy: Defensive');
1163
+ break;
1164
+ case '3':
1165
+ this.addEventLog('Strategy: Balanced');
1166
+ break;
1167
+ case '4':
1168
+ this.addEventLog('Strategy: Skill Focus');
1169
+ break;
1170
+ case 'escape':
1171
+ // Can't escape combat
1172
+ this.addEventLog('Cannot flee from combat!');
1173
+ break;
1174
+ }
1175
+ }
1176
+ /**
1177
+ * Handle travel keypress
1178
+ */
1179
+ handleTravelKey(key) {
1180
+ if (key.name === 'escape') {
1181
+ if (this.onCommand) {
1182
+ this.onCommand('cancel_travel');
1183
+ }
1184
+ }
1185
+ }
1186
+ /**
1187
+ * Handle menu keypress
1188
+ */
1189
+ handleMenuKey(key) {
1190
+ // Menu navigation
1191
+ }
1192
+ /**
1193
+ * Add event to log
1194
+ */
1195
+ addEventLog(message) {
1196
+ const time = new Date().toLocaleTimeString('en-US', { hour12: false }).slice(0, 5);
1197
+ this.eventLog.push(`[${time}] ${message}`);
1198
+ if (this.eventLog.length > this.maxLogLines) {
1199
+ this.eventLog.shift();
1200
+ }
1201
+ // Also update the events section if using new architecture
1202
+ if (this.useSectionArchitecture) {
1203
+ this.eventsSection.addEvent(message);
1204
+ }
1205
+ }
1206
+ /**
1207
+ * Register command handler
1208
+ */
1209
+ onCommandInput(handler) {
1210
+ this.onCommand = handler;
1211
+ }
1212
+ /**
1213
+ * Register dialog handler
1214
+ */
1215
+ onDialogInputHandler(handler) {
1216
+ this.onDialogInput = handler;
1217
+ }
1218
+ // =========================================================================
1219
+ // RENDERING
1220
+ // =========================================================================
1221
+ /**
1222
+ * Render the main game screen using section-based architecture
1223
+ */
1224
+ renderMainScreen(state, npcsInRoom = [], activeQuests = []) {
1225
+ // Cache state for refresh
1226
+ this.cachedState = state;
1227
+ this.cachedNPCs = npcsInRoom;
1228
+ this.cachedQuests = activeQuests;
1229
+ // Skip rendering main screen if a modal is active (modal has its own render)
1230
+ if (this.activeModal) {
1231
+ return;
1232
+ }
1233
+ // Update input buffer from InputManager
1234
+ this.inputBuffer = this.input.getInputBuffer();
1235
+ // Use new section-based architecture
1236
+ if (this.useSectionArchitecture) {
1237
+ this.renderMainScreenWithSections(state, npcsInRoom, activeQuests);
1238
+ return;
1239
+ }
1240
+ // Legacy rendering (kept for fallback)
1241
+ this.renderMainScreenLegacy(state, npcsInRoom, activeQuests);
1242
+ }
1243
+ /**
1244
+ * Render main screen using new section-based architecture
1245
+ */
1246
+ renderMainScreenWithSections(state, npcsInRoom, activeQuests) {
1247
+ const { width: screenWidth, height: screenHeight } = this.screen.getSize();
1248
+ this.screen.clear();
1249
+ // Apply global padding
1250
+ const px = this.PADDING;
1251
+ const py = this.PADDING;
1252
+ const width = screenWidth - this.PADDING * 2;
1253
+ const height = screenHeight - this.PADDING * 2;
1254
+ // Calculate layout
1255
+ const headerHeight = 3;
1256
+ const eventsHeight = Math.min(8, Math.floor(height * 0.25));
1257
+ const mainAreaHeight = height - headerHeight - eventsHeight - 2; // -2 for hint bar
1258
+ const menuWidth = Math.min(30, Math.floor(width * 0.32));
1259
+ const leftWidth = width - menuWidth - 1;
1260
+ const mapHeight = Math.min(10, Math.floor(mainAreaHeight * 0.55));
1261
+ const statusHeight = mainAreaHeight - mapHeight;
1262
+ // Render header (custom, not a section)
1263
+ this.renderHeader(state.player, width, px, py);
1264
+ // Update sections with data
1265
+ this.mapSection.update(state);
1266
+ this.statusSection.update({
1267
+ state: state,
1268
+ npcs: npcsInRoom,
1269
+ quests: activeQuests,
1270
+ });
1271
+ this.actionsSection.update({
1272
+ npcs: npcsInRoom,
1273
+ quests: activeQuests,
1274
+ });
1275
+ // Events are updated via addEventLog
1276
+ // Calculate and set section bounds
1277
+ const boundsMap = new Map();
1278
+ boundsMap.set('map', {
1279
+ x: px,
1280
+ y: py + headerHeight,
1281
+ width: leftWidth,
1282
+ height: mapHeight,
1283
+ });
1284
+ boundsMap.set('status', {
1285
+ x: px,
1286
+ y: py + headerHeight + mapHeight,
1287
+ width: leftWidth,
1288
+ height: statusHeight,
1289
+ });
1290
+ boundsMap.set('actions', {
1291
+ x: px + leftWidth,
1292
+ y: py + headerHeight,
1293
+ width: menuWidth + 1,
1294
+ height: mainAreaHeight,
1295
+ });
1296
+ boundsMap.set('events', {
1297
+ x: px,
1298
+ y: py + headerHeight + mainAreaHeight,
1299
+ width: width,
1300
+ height: eventsHeight,
1301
+ });
1302
+ // Render all sections
1303
+ for (const section of this.sectionManager.getAll()) {
1304
+ const bounds = boundsMap.get(section.id);
1305
+ if (bounds) {
1306
+ section.bounds = bounds;
1307
+ const ctx = { screen: this.screen, bounds };
1308
+ section.render(ctx);
1309
+ }
1310
+ }
1311
+ // Render hint bar using section manager hints
1312
+ this.renderSectionHintBar(px + 1, py + height - 1, width - 2);
1313
+ // Render to screen
1314
+ this.screen.render();
1315
+ }
1316
+ /**
1317
+ * Render hint bar based on section manager state
1318
+ */
1319
+ renderSectionHintBar(x, y, width) {
1320
+ const isAnyEntered = this.sectionManager.isAnyEntered();
1321
+ let hints;
1322
+ if (isAnyEntered) {
1323
+ const sectionHint = this.sectionManager.getCurrentHint();
1324
+ hints = sectionHint || 'Up/Down: navigate Enter: select Esc: exit';
1325
+ }
1326
+ else {
1327
+ hints = 'Arrows: focus Enter: enter | 1-9: Travel I C Q W [M]enu';
1328
+ }
1329
+ this.screen.write(x, y, hints.slice(0, width), ANSI.fg.gray);
1330
+ }
1331
+ /**
1332
+ * Legacy main screen rendering (fallback)
1333
+ */
1334
+ renderMainScreenLegacy(state, npcsInRoom, activeQuests) {
1335
+ const { width: screenWidth, height: screenHeight } = this.screen.getSize();
1336
+ this.screen.clear();
1337
+ // Apply global padding
1338
+ const px = this.PADDING;
1339
+ const py = this.PADDING;
1340
+ const width = screenWidth - this.PADDING * 2;
1341
+ const height = screenHeight - this.PADDING * 2;
1342
+ // New layout: Events at bottom spanning full width
1343
+ const headerHeight = 3;
1344
+ const eventsHeight = Math.min(8, Math.floor(height * 0.25));
1345
+ const mainAreaHeight = height - headerHeight - eventsHeight - 2; // -2 for hint bar
1346
+ const menuWidth = Math.min(30, Math.floor(width * 0.32));
1347
+ const leftWidth = width - menuWidth - 1;
1348
+ const mapHeight = Math.min(10, Math.floor(mainAreaHeight * 0.55));
1349
+ const statusHeight = mainAreaHeight - mapHeight;
1350
+ // Header
1351
+ this.renderHeader(state.player, width, px, py);
1352
+ // Left side: Map (top) + Status (bottom)
1353
+ this.renderMapPanel(px, py + headerHeight, leftWidth, mapHeight, state);
1354
+ this.renderStatusPanel(px, py + headerHeight + mapHeight, leftWidth, statusHeight, state, npcsInRoom, activeQuests);
1355
+ // Right side: Action menu
1356
+ this.renderActionMenu(px + leftWidth, py + headerHeight, menuWidth + 1, mainAreaHeight, state, npcsInRoom, activeQuests);
1357
+ // Events panel at bottom (full width)
1358
+ this.renderEventsPanel(px, py + headerHeight + mainAreaHeight, width, eventsHeight);
1359
+ // Bottom hint bar
1360
+ this.renderHintBar(px + 1, py + height - 1, width - 2);
1361
+ // Render to screen
1362
+ this.screen.render();
1363
+ }
1364
+ /**
1365
+ * Render action menu panel (right side) - NPCs and Quick actions only
1366
+ */
1367
+ renderActionMenu(x, y, width, height, state, npcs, quests) {
1368
+ // Draw box with focus indicator
1369
+ const isFocused = this.focusedSection === 'actions';
1370
+ const isInside = isFocused && this.inSection;
1371
+ const title = isInside ? '[ Actions ]' : 'Actions';
1372
+ const titleFg = isFocused ? ANSI.fg.black : ANSI.fg.cyan;
1373
+ const titleBg = isFocused ? ANSI.bg.cyan : undefined;
1374
+ this.screen.drawBox(x, y, width, height, title, false, titleFg, titleBg);
1375
+ if (isFocused && !isInside) {
1376
+ // Highlight box border when focused but not inside
1377
+ this.screen.write(x, y, '+', ANSI.fg.yellow);
1378
+ }
1379
+ // Build menu items list
1380
+ this.menuItems = [];
1381
+ let itemIndex = 0;
1382
+ let lineY = y + 1;
1383
+ const maxItems = height - 3;
1384
+ // NPCs section
1385
+ if (npcs.length > 0 && lineY < y + maxItems) {
1386
+ this.screen.write(x + 2, lineY, '* NPCs', ANSI.fg.cyan, undefined, ANSI.bold);
1387
+ lineY++;
1388
+ for (let i = 0; i < Math.min(npcs.length, 4); i++) {
1389
+ if (lineY >= y + maxItems)
1390
+ break;
1391
+ const npc = npcs[i];
1392
+ const rel = npc.playerRelationship > 0 ? '+' : npc.playerRelationship < 0 ? '-' : '';
1393
+ const label = `${npc.name} ${rel}`;
1394
+ this.menuItems.push({ type: 'npc', id: `npc_${i}`, label: npc.name });
1395
+ const isSelected = isInside && this.sectionSelectedIndex === itemIndex;
1396
+ const fg = isSelected ? ANSI.fg.black : ANSI.fg.white;
1397
+ const bg = isSelected ? ANSI.bg.cyan : undefined;
1398
+ const indicator = isSelected ? '>' : ' ';
1399
+ this.screen.write(x + 1, lineY, indicator, ANSI.fg.yellow);
1400
+ this.screen.write(x + 3, lineY, label.slice(0, width - 5).padEnd(width - 5), fg, bg);
1401
+ lineY++;
1402
+ itemIndex++;
1403
+ }
1404
+ lineY++;
1405
+ }
1406
+ // Quick actions section
1407
+ if (lineY < y + height - 2) {
1408
+ this.screen.write(x + 2, lineY, '# Actions', ANSI.fg.cyan, undefined, ANSI.bold);
1409
+ lineY++;
1410
+ const quickActions = [
1411
+ { id: 'inventory', label: '[I] Inventory' },
1412
+ { id: 'character', label: '[C] Character' },
1413
+ { id: 'quests', label: `[Q] Quests (${quests.length})` },
1414
+ { id: 'workshop', label: '[W] Workshop' },
1415
+ ];
1416
+ for (const action of quickActions) {
1417
+ if (lineY >= y + height - 2)
1418
+ break;
1419
+ this.menuItems.push({ type: 'action', id: action.id, label: action.label });
1420
+ const isSelected = isInside && this.sectionSelectedIndex === itemIndex;
1421
+ const fg = isSelected ? ANSI.fg.black : ANSI.fg.white;
1422
+ const bg = isSelected ? ANSI.bg.cyan : undefined;
1423
+ const indicator = isSelected ? '>' : ' ';
1424
+ this.screen.write(x + 1, lineY, indicator, ANSI.fg.yellow);
1425
+ this.screen.write(x + 3, lineY, action.label.slice(0, width - 5).padEnd(width - 5), fg, bg);
1426
+ lineY++;
1427
+ itemIndex++;
1428
+ }
1429
+ }
1430
+ // Navigation hint at bottom
1431
+ const hint = isInside ? 'Esc:exit' : 'Enter';
1432
+ this.screen.write(x + 2, y + height - 1, hint, ANSI.fg.gray);
1433
+ }
1434
+ /**
1435
+ * Render hint bar at bottom
1436
+ */
1437
+ renderHintBar(x, y, width) {
1438
+ let hints;
1439
+ if (this.inSection) {
1440
+ hints = 'Up/Down:navigate Enter:select Esc:exit | I C Q W [M]enu';
1441
+ }
1442
+ else {
1443
+ hints = 'Arrows:focus Enter:enter | 1-9:Travel I C Q W [M]enu';
1444
+ }
1445
+ this.screen.write(x, y, hints.slice(0, width), ANSI.fg.gray);
1446
+ }
1447
+ // Menu selection state
1448
+ menuSelectedIndex = 0;
1449
+ menuItems = [];
1450
+ /**
1451
+ * Render header bar
1452
+ */
1453
+ renderHeader(player, width, x = 0, y = 0) {
1454
+ const s = getStrings();
1455
+ // Top border
1456
+ this.screen.drawBox(x, y, width, 3, '', true);
1457
+ // Title
1458
+ this.screen.write(x + 2, y + 1, 'IDLERPG.TERMINAL', ANSI.fg.cyan, undefined, ANSI.bold);
1459
+ // Player info
1460
+ const playerInfo = `${s.status.level}.${player.stats.level} ${player.name}`;
1461
+ this.screen.write(x + 25, y + 1, playerInfo, ANSI.fg.yellow);
1462
+ // HP bar
1463
+ const hpText = `${s.status.hp}:`;
1464
+ this.screen.write(x + 45, y + 1, hpText, ANSI.fg.white);
1465
+ this.screen.drawProgressBar(x + 49, y + 1, 10, player.stats.hp, player.stats.maxHp, ANSI.fg.red);
1466
+ this.screen.write(x + 60, y + 1, `${player.stats.hp}/${player.stats.maxHp}`, ANSI.fg.white);
1467
+ // Real time
1468
+ const time = new Date().toLocaleTimeString('en-US', { hour12: false });
1469
+ this.screen.write(x + width - time.length - 2, y + 1, `${time}`, ANSI.fg.gray);
1470
+ }
1471
+ /**
1472
+ * Render visual map panel with travel destinations
1473
+ */
1474
+ renderMapPanel(x, y, width, height, state) {
1475
+ // Draw box with focus indicator
1476
+ const isFocused = this.focusedSection === 'map';
1477
+ const isInside = isFocused && this.inSection;
1478
+ const title = isInside ? '[ Map ]' : 'Map';
1479
+ const titleFg = isFocused ? ANSI.fg.black : ANSI.fg.cyan;
1480
+ const titleBg = isFocused ? ANSI.bg.cyan : undefined;
1481
+ this.screen.drawBox(x, y, width, height, title, false, titleFg, titleBg);
1482
+ // Map visualization area
1483
+ const mapX = x + 1;
1484
+ const mapY = y + 1;
1485
+ const mapWidth = width - 2;
1486
+ const mapHeight = height - 4; // Leave room for legend and hints
1487
+ const centerX = mapX + Math.floor(mapWidth / 2);
1488
+ const centerY = mapY + Math.floor(mapHeight / 2);
1489
+ // Current room at center with player symbol
1490
+ const playerSymbol = `[${MAP_SYMBOLS.player}]`;
1491
+ this.screen.write(centerX - 1, centerY, playerSymbol, ANSI.fg.brightGreen);
1492
+ // Draw current room name below player
1493
+ const roomName = state.currentRoom.name.slice(0, 12);
1494
+ this.screen.write(centerX - Math.floor(roomName.length / 2), centerY + 1, roomName, ANSI.fg.yellow);
1495
+ // Draw connected rooms around the center
1496
+ const connections = state.currentRoom.connections;
1497
+ // Position connected rooms based on their index (like cardinal directions)
1498
+ // Layout: positions around center with varying distances
1499
+ const positions = this.getConnectionPositions(connections.length, mapWidth, mapHeight);
1500
+ connections.forEach((roomId, i) => {
1501
+ if (i >= positions.length)
1502
+ return;
1503
+ const pos = positions[i];
1504
+ const room = state.currentMap.rooms.get(roomId);
1505
+ const symbol = room?.isExplored ? this.getRoomSymbol(room) : '?';
1506
+ const name = room?.name?.slice(0, 8) || 'Unknown';
1507
+ const roomScreenX = centerX + pos.dx;
1508
+ const roomScreenY = centerY + pos.dy;
1509
+ // Check if this room is selected (when inside map section)
1510
+ const isSelected = isInside && this.sectionSelectedIndex === i;
1511
+ const roomColor = isSelected ? ANSI.fg.brightCyan : (room?.isExplored ? ANSI.fg.white : ANSI.fg.gray);
1512
+ const bgColor = isSelected ? ANSI.bg.blue : undefined;
1513
+ // Draw room symbol
1514
+ this.screen.write(roomScreenX - 1, roomScreenY, `[${symbol}]`, roomColor, bgColor);
1515
+ // Draw room name (shortened)
1516
+ const labelX = roomScreenX - Math.floor(name.length / 2);
1517
+ this.screen.write(Math.max(mapX, labelX), roomScreenY + (pos.dy >= 0 ? 1 : -1), name.slice(0, mapWidth - 2), isSelected ? ANSI.fg.brightCyan : ANSI.fg.gray);
1518
+ // Draw path/connection line from center to room
1519
+ this.drawConnectionPath(centerX, centerY, roomScreenX, roomScreenY, pos.pathChar);
1520
+ // Draw travel time indicator
1521
+ if (isSelected) {
1522
+ const travelTime = this.estimateTravelTime(state.currentRoom, room);
1523
+ this.screen.write(roomScreenX - 2, roomScreenY + (pos.dy >= 0 ? 2 : -2), `~${travelTime}s`, ANSI.fg.yellow);
1524
+ }
1525
+ // Draw selection number
1526
+ this.screen.write(roomScreenX + 2, roomScreenY, `${i + 1}`, ANSI.fg.cyan);
1527
+ });
1528
+ // Legend at bottom
1529
+ const legendY = y + height - 2;
1530
+ this.screen.write(x + 2, legendY, '[@]You [T]Town [D]Dung [?]Unknown', ANSI.fg.gray);
1531
+ // Hint
1532
+ const hintY = y + height - 1;
1533
+ if (isInside) {
1534
+ this.screen.write(x + 2, hintY, '↑↓:select Enter:travel Esc:exit', ANSI.fg.gray);
1535
+ }
1536
+ else if (isFocused) {
1537
+ this.screen.write(x + 2, hintY, 'Enter:select destination', ANSI.fg.gray);
1538
+ }
1539
+ }
1540
+ /**
1541
+ * Get positions for connected rooms around the center
1542
+ */
1543
+ getConnectionPositions(count, mapWidth, mapHeight) {
1544
+ // Calculate spacing based on map size
1545
+ const maxDx = Math.floor(mapWidth / 3);
1546
+ const maxDy = Math.floor(mapHeight / 3);
1547
+ // Predefined positions for up to 8 connections (like compass directions)
1548
+ const allPositions = [
1549
+ { dx: 0, dy: -maxDy, pathChar: '|' }, // North
1550
+ { dx: maxDx, dy: 0, pathChar: '-' }, // East
1551
+ { dx: 0, dy: maxDy, pathChar: '|' }, // South
1552
+ { dx: -maxDx, dy: 0, pathChar: '-' }, // West
1553
+ { dx: maxDx - 2, dy: -maxDy + 1, pathChar: '/' }, // NE
1554
+ { dx: maxDx - 2, dy: maxDy - 1, pathChar: '\\' }, // SE
1555
+ { dx: -maxDx + 2, dy: maxDy - 1, pathChar: '/' }, // SW
1556
+ { dx: -maxDx + 2, dy: -maxDy + 1, pathChar: '\\' }, // NW
1557
+ ];
1558
+ return allPositions.slice(0, count);
1559
+ }
1560
+ /**
1561
+ * Draw a path between two points
1562
+ */
1563
+ drawConnectionPath(fromX, fromY, toX, toY, pathChar) {
1564
+ const dx = toX - fromX;
1565
+ const dy = toY - fromY;
1566
+ const steps = Math.max(Math.abs(dx), Math.abs(dy));
1567
+ if (steps <= 2)
1568
+ return; // Too short to draw path
1569
+ // Draw path characters between the two points
1570
+ for (let i = 1; i < steps; i++) {
1571
+ const t = i / steps;
1572
+ const px = Math.round(fromX + dx * t);
1573
+ const py = Math.round(fromY + dy * t);
1574
+ // Skip positions near the endpoints (where room symbols are)
1575
+ if (i < 2 || i > steps - 2)
1576
+ continue;
1577
+ // Choose appropriate path character
1578
+ let char = '.';
1579
+ if (Math.abs(dx) > Math.abs(dy) * 2) {
1580
+ char = '-';
1581
+ }
1582
+ else if (Math.abs(dy) > Math.abs(dx) * 2) {
1583
+ char = '|';
1584
+ }
1585
+ else {
1586
+ char = pathChar;
1587
+ }
1588
+ this.screen.write(px, py, char, ANSI.fg.gray);
1589
+ }
1590
+ }
1591
+ /**
1592
+ * Estimate travel time based on room distance
1593
+ */
1594
+ estimateTravelTime(from, to) {
1595
+ if (!to)
1596
+ return 5;
1597
+ // Calculate based on position difference
1598
+ const dx = Math.abs(to.position.x - from.position.x);
1599
+ const dy = Math.abs(to.position.y - from.position.y);
1600
+ const dz = Math.abs(to.position.floor - from.position.floor);
1601
+ // Base time: 3-10 seconds depending on distance
1602
+ const distance = Math.sqrt(dx * dx + dy * dy) + dz * 2;
1603
+ return Math.max(3, Math.min(15, Math.floor(distance * 2 + 3)));
1604
+ }
1605
+ /**
1606
+ * Get room symbol
1607
+ */
1608
+ getRoomSymbol(room) {
1609
+ const symbols = {
1610
+ town: 'T',
1611
+ dungeon_entrance: 'D',
1612
+ combat: '!',
1613
+ shop: 'S',
1614
+ treasure: '$',
1615
+ boss: 'B',
1616
+ event: 'E',
1617
+ rest: 'R',
1618
+ empty: '·',
1619
+ };
1620
+ return symbols[room.type] || '?';
1621
+ }
1622
+ /**
1623
+ * Render status panel (compact, without events)
1624
+ */
1625
+ renderStatusPanel(x, y, width, height, state, npcs, quests) {
1626
+ // Draw box with focus indicator
1627
+ const isFocused = this.focusedSection === 'status';
1628
+ const title = 'Status';
1629
+ const titleFg = isFocused ? ANSI.fg.black : ANSI.fg.cyan;
1630
+ const titleBg = isFocused ? ANSI.bg.cyan : undefined;
1631
+ this.screen.drawBox(x, y, width, height, title, false, titleFg, titleBg);
1632
+ let lineY = y + 1;
1633
+ // Gold
1634
+ const gold = `Gold: ${state.player.stats.gold}`;
1635
+ this.screen.write(x + 2, lineY, gold, ANSI.fg.yellow);
1636
+ lineY++;
1637
+ // Equipment
1638
+ if (state.player.equipment.weapon) {
1639
+ this.screen.write(x + 2, lineY, `Weapon: ${state.player.equipment.weapon.name}`, ANSI.fg.white);
1640
+ }
1641
+ else {
1642
+ this.screen.write(x + 2, lineY, 'Weapon: (none)', ANSI.fg.gray);
1643
+ }
1644
+ lineY++;
1645
+ // Active quest (if any)
1646
+ if (quests.length > 0 && lineY < y + height - 1) {
1647
+ const quest = quests[0];
1648
+ const completed = quest.objectives.filter(o => o.isComplete).length;
1649
+ const total = quest.objectives.length;
1650
+ const questText = `Quest: ${quest.name} [${completed}/${total}]`;
1651
+ this.screen.write(x + 2, lineY, questText.slice(0, width - 4), ANSI.fg.magenta);
1652
+ lineY++;
1653
+ }
1654
+ // NPCs count
1655
+ if (npcs.length > 0 && lineY < y + height - 1) {
1656
+ this.screen.write(x + 2, lineY, `NPCs: ${npcs.length} here`, ANSI.fg.cyan);
1657
+ }
1658
+ }
1659
+ /**
1660
+ * Render events panel (spans bottom of screen) with scrolling
1661
+ */
1662
+ renderEventsPanel(x, y, width, height) {
1663
+ // Draw box with focus indicator
1664
+ const isFocused = this.focusedSection === 'events';
1665
+ const isInside = isFocused && this.inSection;
1666
+ const title = isInside ? '[ Events ]' : 'Events';
1667
+ const titleFg = isFocused ? ANSI.fg.black : ANSI.fg.cyan;
1668
+ const titleBg = isFocused ? ANSI.bg.cyan : undefined;
1669
+ this.screen.drawBox(x, y, width, height, title, false, titleFg, titleBg);
1670
+ const maxLogLines = height - 2;
1671
+ const logStartY = y + 1;
1672
+ // When inside events, use sectionScrollOffset for scrolling
1673
+ const scrollOffset = isInside ? this.sectionScrollOffset : Math.max(0, this.eventLog.length - maxLogLines);
1674
+ // Bound the selected index and scroll offset
1675
+ if (isInside) {
1676
+ const maxIndex = Math.max(0, this.eventLog.length - 1);
1677
+ if (this.sectionSelectedIndex > maxIndex) {
1678
+ this.sectionSelectedIndex = maxIndex;
1679
+ }
1680
+ }
1681
+ for (let i = 0; i < Math.min(this.eventLog.length, maxLogLines); i++) {
1682
+ const logIndex = scrollOffset + i;
1683
+ if (logIndex >= this.eventLog.length)
1684
+ break;
1685
+ const isSelected = isInside && logIndex === this.sectionSelectedIndex;
1686
+ const fg = isSelected ? ANSI.fg.white : ANSI.fg.gray;
1687
+ const bg = isSelected ? ANSI.bg.blue : undefined;
1688
+ const indicator = isSelected ? '>' : ' ';
1689
+ this.screen.write(x, logStartY + i, indicator, ANSI.fg.yellow);
1690
+ this.screen.write(x + 1, logStartY + i, this.eventLog[logIndex].slice(0, width - 3).padEnd(width - 3), fg, bg);
1691
+ }
1692
+ // Scroll indicator
1693
+ if (this.eventLog.length > maxLogLines) {
1694
+ const canScrollUp = scrollOffset > 0;
1695
+ const canScrollDown = scrollOffset + maxLogLines < this.eventLog.length;
1696
+ if (canScrollUp)
1697
+ this.screen.write(x + width - 2, y, '^', ANSI.fg.yellow);
1698
+ if (canScrollDown)
1699
+ this.screen.write(x + width - 2, y + height - 1, 'v', ANSI.fg.yellow);
1700
+ }
1701
+ // Hint
1702
+ const hint = isInside ? 'Esc:exit' : '';
1703
+ if (hint)
1704
+ this.screen.write(x + 2, y + height - 1, hint, ANSI.fg.gray);
1705
+ }
1706
+ /**
1707
+ * Render event log
1708
+ */
1709
+ renderEventLog(x, y, width, maxLines) {
1710
+ this.screen.drawBox(x, y, width, maxLines + 2, 'Event Log');
1711
+ for (let i = 0; i < Math.min(this.eventLog.length, maxLines); i++) {
1712
+ const logIndex = Math.max(0, this.eventLog.length - maxLines) + i;
1713
+ if (this.eventLog[logIndex]) {
1714
+ this.screen.write(x + 2, y + 1 + i, this.eventLog[logIndex].slice(0, width - 4), ANSI.fg.gray);
1715
+ }
1716
+ }
1717
+ }
1718
+ /**
1719
+ * Render input area
1720
+ */
1721
+ renderInputArea(x, y, width) {
1722
+ this.screen.drawBox(x, y, width, 3, '');
1723
+ const promptText = this.inputPrompt + this.inputBuffer + '▌';
1724
+ this.screen.write(x + 2, y + 1, promptText.slice(0, width - 4), ANSI.fg.white);
1725
+ }
1726
+ // =========================================================================
1727
+ // COMBAT MODE
1728
+ // =========================================================================
1729
+ /**
1730
+ * Render combat screen
1731
+ */
1732
+ renderCombatScreen(combat, player) {
1733
+ // Skip if modal is active
1734
+ if (this.activeModal)
1735
+ return;
1736
+ const { width: screenWidth, height: screenHeight } = this.screen.getSize();
1737
+ this.screen.clear();
1738
+ // Apply global padding
1739
+ const px = this.PADDING;
1740
+ const py = this.PADDING;
1741
+ const width = screenWidth - this.PADDING * 2;
1742
+ const height = screenHeight - this.PADDING * 2;
1743
+ // Combat box
1744
+ const boxWidth = Math.min(70, width - 4);
1745
+ const boxHeight = Math.min(25, height - 4);
1746
+ const boxX = px + Math.floor((width - boxWidth) / 2);
1747
+ const boxY = py + Math.floor((height - boxHeight) / 2);
1748
+ this.screen.drawBox(boxX, boxY, boxWidth, boxHeight, '== COMBAT ==', true);
1749
+ let lineY = boxY + 2;
1750
+ // Player HP
1751
+ this.screen.write(boxX + 3, lineY, `[${player.name}]`, ANSI.fg.brightGreen);
1752
+ lineY++;
1753
+ this.screen.write(boxX + 3, lineY, 'HP: ', ANSI.fg.white);
1754
+ this.screen.drawProgressBar(boxX + 7, lineY, 20, combat.playerHp, player.stats.maxHp, ANSI.fg.red);
1755
+ this.screen.write(boxX + 28, lineY, `${combat.playerHp}/${player.stats.maxHp}`, ANSI.fg.white);
1756
+ lineY += 2;
1757
+ // Enemies
1758
+ this.screen.write(boxX + 3, lineY, '─── Enemies ───', ANSI.fg.cyan);
1759
+ lineY++;
1760
+ for (const enemy of combat.enemies) {
1761
+ const isAlive = enemy.hp > 0;
1762
+ const color = isAlive ? ANSI.fg.red : ANSI.fg.gray;
1763
+ const status = isAlive ? '' : ' [DEFEATED]';
1764
+ this.screen.write(boxX + 3, lineY, `${enemy.name}${status}`, color);
1765
+ lineY++;
1766
+ if (isAlive) {
1767
+ this.screen.write(boxX + 5, lineY, 'HP: ', ANSI.fg.white);
1768
+ this.screen.drawProgressBar(boxX + 9, lineY, 15, enemy.hp, enemy.maxHp, ANSI.fg.red);
1769
+ this.screen.write(boxX + 25, lineY, `${enemy.hp}/${enemy.maxHp}`, ANSI.fg.gray);
1770
+ }
1771
+ lineY++;
1772
+ }
1773
+ lineY++;
1774
+ // Combat log
1775
+ this.screen.write(boxX + 3, lineY, '─── Battle Log ───', ANSI.fg.cyan);
1776
+ lineY++;
1777
+ const logStart = Math.max(0, combat.log.length - 5);
1778
+ for (let i = logStart; i < combat.log.length; i++) {
1779
+ const entry = combat.log[i];
1780
+ this.screen.write(boxX + 3, lineY, `> ${entry.message}`.slice(0, boxWidth - 6), ANSI.fg.gray);
1781
+ lineY++;
1782
+ }
1783
+ // Strategy options
1784
+ lineY = boxY + boxHeight - 4;
1785
+ this.screen.write(boxX + 3, lineY, 'Strategy: [1]Aggressive [2]Defensive [3]Balanced [4]Skills', ANSI.fg.yellow);
1786
+ lineY++;
1787
+ this.screen.write(boxX + 3, lineY, `Current: ${combat.strategy}`, ANSI.fg.white);
1788
+ // Real time
1789
+ const time = new Date().toLocaleTimeString('en-US', { hour12: false });
1790
+ this.screen.write(boxX + boxWidth - 10, boxY + 1, `${time}`, ANSI.fg.gray);
1791
+ this.screen.render();
1792
+ }
1793
+ // =========================================================================
1794
+ // TRAVEL MODE
1795
+ // =========================================================================
1796
+ /**
1797
+ * Render travel screen - keeps the map visible with animated player movement
1798
+ */
1799
+ renderTravelScreen(state, fromRoom, toRoom, progress, duration, events) {
1800
+ // Skip if modal is active
1801
+ if (this.activeModal)
1802
+ return;
1803
+ const { width: screenWidth, height: screenHeight } = this.screen.getSize();
1804
+ this.screen.clear();
1805
+ // Apply global padding
1806
+ const px = this.PADDING;
1807
+ const py = this.PADDING;
1808
+ const width = screenWidth - this.PADDING * 2;
1809
+ const height = screenHeight - this.PADDING * 2;
1810
+ // Same layout as explore mode
1811
+ const headerHeight = 3;
1812
+ const menuWidth = Math.min(32, Math.floor(width * 0.35));
1813
+ const leftWidth = width - menuWidth - 2;
1814
+ const mapHeight = Math.min(14, Math.floor((height - headerHeight - 4) * 0.55));
1815
+ const statusHeight = height - headerHeight - mapHeight - 4;
1816
+ // Header
1817
+ this.renderHeader(state.player, width, px, py);
1818
+ // Left side: Travel Map (with animated player)
1819
+ this.renderTravelMapPanel(px + 1, py + headerHeight, leftWidth, mapHeight, fromRoom, toRoom, progress);
1820
+ // Left side (bottom): Travel status + events
1821
+ this.renderTravelStatusPanel(px + 1, py + headerHeight + mapHeight, leftWidth, statusHeight, fromRoom?.name || 'Unknown', toRoom?.name || 'Unknown', progress, duration, events);
1822
+ // Right side: Travel info panel
1823
+ this.renderTravelInfoPanel(px + leftWidth + 1, py + headerHeight, menuWidth, height - headerHeight - 1, fromRoom?.name || 'Unknown', toRoom?.name || 'Unknown', progress, duration);
1824
+ // Bottom hint bar
1825
+ this.screen.write(px + 1, py + height - 1, '[ESC] Cancel Travel', ANSI.fg.yellow);
1826
+ this.screen.render();
1827
+ }
1828
+ /**
1829
+ * Render the travel map with animated player position
1830
+ */
1831
+ renderTravelMapPanel(x, y, width, height, fromRoom, toRoom, progress) {
1832
+ // Animated title
1833
+ const dots = '.'.repeat(Math.floor(Date.now() / 300) % 4);
1834
+ this.screen.drawBox(x, y, width, height, `Traveling${dots}`);
1835
+ const mapY = y + 2;
1836
+ const mapX = x + 2;
1837
+ const mapWidth = width - 4;
1838
+ const mapHeight = height - 5;
1839
+ const centerX = mapX + Math.floor(mapWidth / 2);
1840
+ const centerY = mapY + Math.floor(mapHeight / 2);
1841
+ if (!fromRoom || !toRoom) {
1842
+ this.screen.write(centerX - 5, centerY, 'Traveling...', ANSI.fg.yellow);
1843
+ return;
1844
+ }
1845
+ // Calculate travel distance for proportional display
1846
+ const fromPos = fromRoom.position;
1847
+ const toPos = toRoom.position;
1848
+ const dx = toPos.x - fromPos.x;
1849
+ const dy = toPos.y - fromPos.y;
1850
+ const dz = toPos.floor - fromPos.floor;
1851
+ // Calculate actual distance (affects visual separation)
1852
+ const distance = Math.sqrt(dx * dx + dy * dy) + Math.abs(dz) * 2;
1853
+ const normalizedDist = Math.min(1, distance / 5); // Normalize to 0-1
1854
+ // Calculate screen positions based on distance
1855
+ const maxOffsetX = Math.floor(mapWidth / 3 * (0.5 + normalizedDist * 0.5));
1856
+ const maxOffsetY = Math.floor(mapHeight / 3 * (0.5 + normalizedDist * 0.5));
1857
+ let fromScreenX = centerX;
1858
+ let fromScreenY = centerY;
1859
+ let toScreenX = centerX;
1860
+ let toScreenY = centerY;
1861
+ // Position rooms based on their relative positions
1862
+ if (Math.abs(dx) > Math.abs(dy)) {
1863
+ fromScreenX = centerX - maxOffsetX;
1864
+ toScreenX = centerX + maxOffsetX;
1865
+ const vertOffset = Math.sign(dy) * Math.min(Math.abs(dy), maxOffsetY / 2);
1866
+ fromScreenY = centerY - Math.floor(vertOffset / 2);
1867
+ toScreenY = centerY + Math.floor(vertOffset / 2);
1868
+ }
1869
+ else {
1870
+ fromScreenY = centerY - maxOffsetY;
1871
+ toScreenY = centerY + maxOffsetY;
1872
+ const horizOffset = Math.sign(dx) * Math.min(Math.abs(dx), maxOffsetX / 2);
1873
+ fromScreenX = centerX - Math.floor(horizOffset / 2);
1874
+ toScreenX = centerX + Math.floor(horizOffset / 2);
1875
+ }
1876
+ // Draw terrain/background dots to show distance
1877
+ for (let ty = mapY; ty < mapY + mapHeight; ty++) {
1878
+ for (let tx = mapX; tx < mapX + mapWidth; tx += 3) {
1879
+ if (Math.random() < 0.3) {
1880
+ this.screen.write(tx, ty, '.', ANSI.fg.gray);
1881
+ }
1882
+ }
1883
+ }
1884
+ // Draw the path between rooms (shows the route)
1885
+ this.drawTravelPath(fromScreenX, fromScreenY, toScreenX, toScreenY);
1886
+ // Draw traveled portion in different color
1887
+ const traveledX = Math.round(fromScreenX + (toScreenX - fromScreenX) * progress);
1888
+ const traveledY = Math.round(fromScreenY + (toScreenY - fromScreenY) * progress);
1889
+ this.drawTraveledPath(fromScreenX, fromScreenY, traveledX, traveledY);
1890
+ // Draw origin room (behind player start)
1891
+ const fromSymbol = this.getRoomSymbol(fromRoom);
1892
+ this.screen.write(fromScreenX - 1, fromScreenY, `[${fromSymbol}]`, ANSI.fg.gray);
1893
+ const fromLabel = fromRoom.name.slice(0, 10);
1894
+ this.screen.write(fromScreenX - Math.floor(fromLabel.length / 2), fromScreenY + 1, fromLabel, ANSI.fg.gray);
1895
+ // Draw destination room (goal)
1896
+ const toSymbol = this.getRoomSymbol(toRoom);
1897
+ this.screen.write(toScreenX - 1, toScreenY, `[${toSymbol}]`, ANSI.fg.brightYellow);
1898
+ const toLabel = toRoom.name.slice(0, 10);
1899
+ this.screen.write(toScreenX - Math.floor(toLabel.length / 2), toScreenY - 1, toLabel, ANSI.fg.yellow);
1900
+ // Calculate and draw player position (interpolated based on progress)
1901
+ const playerX = Math.round(fromScreenX + (toScreenX - fromScreenX) * progress);
1902
+ const playerY = Math.round(fromScreenY + (toScreenY - fromScreenY) * progress);
1903
+ // Animated player symbol
1904
+ const playerFrame = Date.now() % 500 < 250 ? '@' : '*';
1905
+ this.screen.write(playerX - 1, playerY, `[${playerFrame}]`, ANSI.fg.brightGreen);
1906
+ // Progress percentage on the side
1907
+ const pct = Math.floor(progress * 100);
1908
+ this.screen.write(x + width - 6, y + 1, `${pct}%`, ANSI.fg.cyan);
1909
+ // Legend and distance info at bottom
1910
+ const legendY = y + height - 2;
1911
+ const distInfo = `Distance: ${Math.floor(distance * 10)}m`;
1912
+ this.screen.write(x + 2, legendY, distInfo, ANSI.fg.gray);
1913
+ this.screen.write(x + width - 20, legendY, '[@]You -> [*]Dest', ANSI.fg.gray);
1914
+ }
1915
+ /**
1916
+ * Draw the traveled portion of the path in a different color
1917
+ */
1918
+ drawTraveledPath(fromX, fromY, toX, toY) {
1919
+ const dx = toX - fromX;
1920
+ const dy = toY - fromY;
1921
+ const steps = Math.max(Math.abs(dx), Math.abs(dy));
1922
+ if (steps === 0)
1923
+ return;
1924
+ for (let i = 1; i < steps; i++) {
1925
+ const t = i / steps;
1926
+ const px = Math.round(fromX + dx * t);
1927
+ const py = Math.round(fromY + dy * t);
1928
+ // Use a brighter character for traveled path
1929
+ this.screen.write(px, py, '=', ANSI.fg.green);
1930
+ }
1931
+ }
1932
+ /**
1933
+ * Draw a path between two points on the map
1934
+ */
1935
+ drawTravelPath(fromX, fromY, toX, toY) {
1936
+ const dx = toX - fromX;
1937
+ const dy = toY - fromY;
1938
+ const steps = Math.max(Math.abs(dx), Math.abs(dy));
1939
+ if (steps === 0)
1940
+ return;
1941
+ for (let i = 1; i < steps; i++) {
1942
+ const t = i / steps;
1943
+ const px = Math.round(fromX + dx * t);
1944
+ const py = Math.round(fromY + dy * t);
1945
+ // Choose path character based on direction
1946
+ let pathChar = '·';
1947
+ if (Math.abs(dx) > Math.abs(dy) * 2) {
1948
+ pathChar = '─';
1949
+ }
1950
+ else if (Math.abs(dy) > Math.abs(dx) * 2) {
1951
+ pathChar = '│';
1952
+ }
1953
+ else if ((dx > 0 && dy > 0) || (dx < 0 && dy < 0)) {
1954
+ pathChar = '╲';
1955
+ }
1956
+ else {
1957
+ pathChar = '╱';
1958
+ }
1959
+ this.screen.write(px, py, pathChar, ANSI.fg.gray);
1960
+ }
1961
+ }
1962
+ /**
1963
+ * Render travel status panel with progress and events
1964
+ */
1965
+ renderTravelStatusPanel(x, y, width, height, fromName, toName, progress, duration, events) {
1966
+ this.screen.drawBox(x, y, width, height, 'Journey');
1967
+ let lineY = y + 1;
1968
+ // Progress bar
1969
+ const progressPercent = Math.floor(progress * 100);
1970
+ this.screen.write(x + 2, lineY, `Progress: ${progressPercent}%`, ANSI.fg.white);
1971
+ lineY++;
1972
+ this.screen.drawProgressBar(x + 2, lineY, Math.min(40, width - 6), progress, 1);
1973
+ lineY++;
1974
+ // Time remaining
1975
+ const remaining = Math.ceil(duration * (1 - progress));
1976
+ this.screen.write(x + 2, lineY, `Time remaining: ${remaining}s`, ANSI.fg.gray);
1977
+ lineY += 2;
1978
+ // Events during travel
1979
+ if (events.length > 0 && lineY < y + height - 1) {
1980
+ this.screen.write(x + 2, lineY, '─ Events ─', ANSI.fg.cyan);
1981
+ lineY++;
1982
+ const maxEvents = Math.min(events.length, height - (lineY - y) - 1);
1983
+ for (let i = 0; i < maxEvents; i++) {
1984
+ const event = events[events.length - maxEvents + i];
1985
+ this.screen.write(x + 2, lineY, `• ${event}`.slice(0, width - 4), ANSI.fg.white);
1986
+ lineY++;
1987
+ }
1988
+ }
1989
+ }
1990
+ /**
1991
+ * Render travel info panel (right side)
1992
+ */
1993
+ renderTravelInfoPanel(x, y, width, height, fromName, toName, progress, duration) {
1994
+ this.screen.drawBox(x, y, width, height, 'Travel');
1995
+ let lineY = y + 1;
1996
+ // From/To info
1997
+ this.screen.write(x + 2, lineY, '> Journey', ANSI.fg.cyan, undefined, ANSI.bold);
1998
+ lineY += 2;
1999
+ this.screen.write(x + 2, lineY, 'From:', ANSI.fg.gray);
2000
+ lineY++;
2001
+ this.screen.write(x + 3, lineY, fromName.slice(0, width - 5), ANSI.fg.white);
2002
+ lineY += 2;
2003
+ this.screen.write(x + 2, lineY, 'To:', ANSI.fg.yellow);
2004
+ lineY++;
2005
+ this.screen.write(x + 3, lineY, toName.slice(0, width - 5), ANSI.fg.brightYellow);
2006
+ lineY += 2;
2007
+ // Visual progress indicator
2008
+ const progressPercent = Math.floor(progress * 100);
2009
+ this.screen.write(x + 2, lineY, '─────────────', ANSI.fg.gray);
2010
+ lineY++;
2011
+ // Animated travel indicator
2012
+ const barWidth = width - 6;
2013
+ const filledWidth = Math.floor(progress * barWidth);
2014
+ const travelerFrame = ['>', '>'][Math.floor(Date.now() / 300) % 2];
2015
+ this.screen.write(x + 2, lineY, '·'.repeat(barWidth), ANSI.fg.gray);
2016
+ this.screen.write(x + 2, lineY, '═'.repeat(filledWidth), ANSI.fg.green);
2017
+ if (filledWidth < barWidth) {
2018
+ this.screen.write(x + 2 + filledWidth, lineY, travelerFrame, ANSI.fg.brightGreen);
2019
+ }
2020
+ lineY++;
2021
+ this.screen.write(x + 2, lineY, `${progressPercent}% complete`, ANSI.fg.cyan);
2022
+ lineY += 2;
2023
+ // Time info
2024
+ const remaining = Math.ceil(duration * (1 - progress));
2025
+ const elapsed = Math.floor(duration * progress);
2026
+ this.screen.write(x + 2, lineY, `⏱ ${remaining}s remaining`, ANSI.fg.gray);
2027
+ lineY++;
2028
+ this.screen.write(x + 2, lineY, ` ${elapsed}s traveled`, ANSI.fg.gray);
2029
+ lineY += 2;
2030
+ // Cancel hint at bottom
2031
+ const hintY = y + height - 3;
2032
+ this.screen.write(x + 2, hintY, '─────────────', ANSI.fg.gray);
2033
+ this.screen.write(x + 2, hintY + 1, '[ESC] Cancel', ANSI.fg.yellow);
2034
+ }
2035
+ // =========================================================================
2036
+ // DIALOG MODE
2037
+ // =========================================================================
2038
+ /**
2039
+ * Render dialog screen
2040
+ */
2041
+ renderDialogScreen(npc, history, options, isWaiting) {
2042
+ // Cache dialog state for refresh
2043
+ this.dialogNPC = npc;
2044
+ this.dialogHistory = history;
2045
+ this.dialogOptions = options;
2046
+ this.isWaitingForAI = isWaiting;
2047
+ // Skip if modal is active
2048
+ if (this.activeModal)
2049
+ return;
2050
+ // Update input buffer from InputManager
2051
+ this.inputBuffer = this.input.getInputBuffer();
2052
+ const { width: screenWidth, height: screenHeight } = this.screen.getSize();
2053
+ this.screen.clear();
2054
+ // Apply global padding
2055
+ const px = this.PADDING;
2056
+ const py = this.PADDING;
2057
+ const width = screenWidth - this.PADDING * 2;
2058
+ const height = screenHeight - this.PADDING * 2;
2059
+ const boxWidth = Math.min(70, width - 4);
2060
+ const boxHeight = Math.min(25, height - 4);
2061
+ const boxX = px + Math.floor((width - boxWidth) / 2);
2062
+ const boxY = py + Math.floor((height - boxHeight) / 2);
2063
+ this.screen.drawBox(boxX, boxY, boxWidth, boxHeight, `${npc.name} (${npc.role})`, true);
2064
+ let lineY = boxY + 2;
2065
+ // NPC info
2066
+ this.screen.write(boxX + 3, lineY, npc.appearance.slice(0, boxWidth - 6), ANSI.fg.gray);
2067
+ lineY += 2;
2068
+ // Dialog history
2069
+ const maxHistory = boxHeight - 12;
2070
+ const historyStart = Math.max(0, history.length - maxHistory);
2071
+ for (let i = historyStart; i < history.length; i++) {
2072
+ const entry = history[i];
2073
+ const prefix = entry.role === 'player' ? '[You] ' : `[${npc.name}] `;
2074
+ const color = entry.role === 'player' ? ANSI.fg.cyan : ANSI.fg.yellow;
2075
+ this.screen.write(boxX + 3, lineY, prefix, color);
2076
+ // Word wrap
2077
+ const textWidth = boxWidth - 8;
2078
+ const text = entry.text;
2079
+ for (let j = 0; j < text.length && lineY < boxY + boxHeight - 6; j += textWidth) {
2080
+ const line = j === 0 ? text.slice(j, j + textWidth - prefix.length) : text.slice(j, j + textWidth);
2081
+ this.screen.write(j === 0 ? boxX + 3 + prefix.length : boxX + 3, lineY, line, ANSI.fg.white);
2082
+ lineY++;
2083
+ }
2084
+ }
2085
+ // Waiting indicator
2086
+ if (isWaiting) {
2087
+ this.screen.write(boxX + 3, lineY, '▌ Thinking...', ANSI.fg.gray);
2088
+ lineY++;
2089
+ }
2090
+ // Quick options
2091
+ lineY = boxY + boxHeight - 6;
2092
+ this.screen.write(boxX + 3, lineY, '─── Quick Options ───', ANSI.fg.cyan);
2093
+ lineY++;
2094
+ for (const option of options.slice(0, 3)) {
2095
+ this.screen.write(boxX + 3, lineY, `[${option.key}] ${option.text}`, ANSI.fg.gray);
2096
+ lineY++;
2097
+ }
2098
+ // Input area
2099
+ lineY = boxY + boxHeight - 2;
2100
+ this.screen.write(boxX + 3, lineY, 'Say: ' + this.inputBuffer + '▌', ANSI.fg.white);
2101
+ // Real time
2102
+ const time = new Date().toLocaleTimeString('en-US', { hour12: false });
2103
+ this.screen.write(boxX + boxWidth - 10, boxY + 1, `${time}`, ANSI.fg.gray);
2104
+ this.screen.render();
2105
+ }
2106
+ }
2107
+ // Singleton
2108
+ let uiInstance = null;
2109
+ export function getGameUI() {
2110
+ if (!uiInstance) {
2111
+ uiInstance = new GameUI();
2112
+ }
2113
+ return uiInstance;
2114
+ }
2115
+ //# sourceMappingURL=GameUI.js.map
2116
+ //# debugId=7d089040-391e-5d75-84d0-9dd7c0b0e3ea