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