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