diff --git a/ASCII-G.vcxproj b/ASCII-G.vcxproj index 603b553..291e33f 100644 --- a/ASCII-G.vcxproj +++ b/ASCII-G.vcxproj @@ -127,13 +127,16 @@ + + + diff --git a/ASCII-G.vcxproj.filters b/ASCII-G.vcxproj.filters index bba9111..d24560e 100644 --- a/ASCII-G.vcxproj.filters +++ b/ASCII-G.vcxproj.filters @@ -30,6 +30,9 @@ Исходные файлы + + Исходные файлы + @@ -47,5 +50,11 @@ Файлы заголовков + + Файлы заголовков + + + Файлы заголовков + \ No newline at end of file diff --git a/data/locations/location_1.txt b/data/locations/location_1.txt index 26a2980..83c4c94 100644 --- a/data/locations/location_1.txt +++ b/data/locations/location_1.txt @@ -14,7 +14,7 @@ RIDDLE_ANSWER=hole MAP_BEGIN ########## #@.......# -#....C...# +#..N.C...# #........# #......D.# ########## diff --git a/data/locations/location_2.txt b/data/locations/location_2.txt index d6b500a..afba5b9 100644 --- a/data/locations/location_2.txt +++ b/data/locations/location_2.txt @@ -14,7 +14,7 @@ RIDDLE_ANSWER=magic MAP_BEGIN ########## #@.......# -#..E.....# +#..N..C..# #........# #......D.# ########## diff --git a/data/locations/location_3.txt b/data/locations/location_3.txt index 1bae725..befdbb1 100644 --- a/data/locations/location_3.txt +++ b/data/locations/location_3.txt @@ -16,6 +16,6 @@ MAP_BEGIN #@.......# #....N...# #........# -#......D.# +#...C..D.# ########## MAP_END \ No newline at end of file diff --git a/data/locations/location_4.txt b/data/locations/location_4.txt index aca674e..a99ca3a 100644 --- a/data/locations/location_4.txt +++ b/data/locations/location_4.txt @@ -15,7 +15,7 @@ MAP_BEGIN ########## #@.......# #....C...# -#..E.....# +#..N.....# #......D.# ########## MAP_END \ No newline at end of file diff --git a/data/locations/location_5.txt b/data/locations/location_5.txt index 471f11d..247e2b1 100644 --- a/data/locations/location_5.txt +++ b/data/locations/location_5.txt @@ -14,7 +14,7 @@ RIDDLE_ANSWER=keyboard MAP_BEGIN ########## #@.......# -#....E...# +#....N...# #....C...# #......D.# ########## diff --git a/data/npcs/npc_1.txt b/data/npcs/npc_1.txt new file mode 100644 index 0000000..213df69 --- /dev/null +++ b/data/npcs/npc_1.txt @@ -0,0 +1,23 @@ +ID=1 +NAME=Old Guard +STATE=FRIENDLY +HP=60 +DAMAGE=8 + +DIALOG_TEXT=The guard looks tired. He asks: "What do you seek here?" +OPTION_1=I seek knowledge. +RESULT_1=GIVE_ITEM +VALUE_1=101 +MESSAGE_1=The guard gives you a small healing potion. + +OPTION_2=Move away, old man. +RESULT_2=BECOME_HOSTILE +VALUE_2=0 +MESSAGE_2=The guard becomes angry and attacks you. + +OPTION_3=Can you open the next door? +RESULT_3=OPEN_LOCATION +VALUE_3=1 +MESSAGE_3=The guard unlocks the next location. + +END \ No newline at end of file diff --git a/data/npcs/npc_2.txt b/data/npcs/npc_2.txt new file mode 100644 index 0000000..dc235f0 --- /dev/null +++ b/data/npcs/npc_2.txt @@ -0,0 +1,13 @@ +ID=2 +NAME=Dungeon Bandit +STATE=HOSTILE +HP=45 +DAMAGE=12 + +DIALOG_TEXT=The bandit blocks your path. +OPTION_1=Fight. +RESULT_1=FIGHT +VALUE_1=0 +MESSAGE_1=The fight begins. + +END \ No newline at end of file diff --git a/data/npcs/npc_3.txt b/data/npcs/npc_3.txt new file mode 100644 index 0000000..71b0f9b --- /dev/null +++ b/data/npcs/npc_3.txt @@ -0,0 +1,23 @@ +ID=3 +NAME=Archivist +STATE=FRIENDLY +HP=50 +DAMAGE=6 + +DIALOG_TEXT=The archivist whispers: "Only those who understand patterns may pass." +OPTION_1=Ask about the sequence. +RESULT_1=OPEN_LOCATION +VALUE_1=3 +MESSAGE_1=The archivist nods and unlocks the door. + +OPTION_2=Demand his notes. +RESULT_2=BECOME_HOSTILE +VALUE_2=0 +MESSAGE_2=The archivist becomes hostile. + +OPTION_3=Ask for help. +RESULT_3=GIVE_ITEM +VALUE_3=102 +MESSAGE_3=The archivist gives you a big healing potion. + +END \ No newline at end of file diff --git a/data/npcs/npc_4.txt b/data/npcs/npc_4.txt new file mode 100644 index 0000000..ae711f1 --- /dev/null +++ b/data/npcs/npc_4.txt @@ -0,0 +1,23 @@ +ID=4 +NAME=Library Keeper +STATE=FRIENDLY +HP=70 +DAMAGE=10 + +DIALOG_TEXT=The keeper stands near the shelves. "Knowledge has a price." +OPTION_1=Respect the library. +RESULT_1=OPEN_LOCATION +VALUE_1=4 +MESSAGE_1=The keeper allows you to pass. + +OPTION_2=Steal a book. +RESULT_2=BECOME_HOSTILE +VALUE_2=0 +MESSAGE_2=The keeper attacks you for stealing. + +OPTION_3=Ask for supplies. +RESULT_3=GIVE_ITEM +VALUE_3=101 +MESSAGE_3=The keeper gives you a small healing potion. + +END \ No newline at end of file diff --git a/data/npcs/npc_5.txt b/data/npcs/npc_5.txt new file mode 100644 index 0000000..4ca607d --- /dev/null +++ b/data/npcs/npc_5.txt @@ -0,0 +1,23 @@ +ID=5 +NAME=Gate Warden +STATE=HOSTILE +HP=100 +DAMAGE=16 + +DIALOG_TEXT=The warden blocks the final gate. +OPTION_1=Fight. +RESULT_1=FIGHT +VALUE_1=0 +MESSAGE_1=The final battle begins. + +OPTION_2=Try to negotiate. +RESULT_2=NOTHING +VALUE_2=0 +MESSAGE_2=The warden ignores your words. + +OPTION_3=Run away. +RESULT_3=NOTHING +VALUE_3=0 +MESSAGE_3=You step back from the gate. + +END \ No newline at end of file diff --git a/include/game.h b/include/game.h index 32d5cb3..2e9de09 100644 --- a/include/game.h +++ b/include/game.h @@ -2,6 +2,7 @@ #include "player.h" #include "location.h" #include "item.h" +#include "npc.h" #include #include @@ -15,6 +16,7 @@ private: Player player; std::vector locations; std::vector itemDatabase; + std::vector npcs; void showMainMenu(); @@ -25,6 +27,10 @@ private: Location* getCurrentLocation(); + void interactWithNPC(Location& location); + void startCombat(NPC& npc); + void applyDialogResult(NPC& npc, const DialogOption& option); + void render(); void processCommand(); diff --git a/include/location.h b/include/location.h index 7c30718..429d690 100644 --- a/include/location.h +++ b/include/location.h @@ -2,10 +2,8 @@ #include #include -struct Point { - int x = 0; - int y = 0; -}; +#include "Point.h" +#include "NPC.h" struct Chest { Point pos; @@ -19,11 +17,6 @@ struct Enemy { int hp = 50; }; -struct NPC { - Point pos; - std::string name; -}; - struct Riddle { int type = 0; std::string text; diff --git a/include/npc.h b/include/npc.h new file mode 100644 index 0000000..edaf650 --- /dev/null +++ b/include/npc.h @@ -0,0 +1,48 @@ +#pragma once +#include +#include +#include "point.h" + +enum class NPCState { + Friendly, + Hostile +}; + +enum class DialogResultType { + GiveItem, + BecomeHostile, + OpenLocation, + Fight, + Nothing +}; + +struct DialogOption { + std::string text; + DialogResultType result = DialogResultType::Nothing; + int value = 0; + std::string message; +}; + +class NPC { +public: + int id = 0; + std::string name; + + NPCState state = NPCState::Friendly; + + int hp = 100; + int damage = 10; + + Point position; + + std::string dialogText; + std::vector options; + + bool defeated = false; + + bool loadFromFile(const std::string& path); + +private: + NPCState parseState(const std::string& value); + DialogResultType parseDialogResult(const std::string& value); +}; \ No newline at end of file diff --git a/include/point.h b/include/point.h new file mode 100644 index 0000000..a49f285 --- /dev/null +++ b/include/point.h @@ -0,0 +1,6 @@ +#pragma once + +struct Point { + int x = 0; + int y = 0; +}; \ No newline at end of file diff --git a/src/game.cpp b/src/game.cpp index aa352af..0317fae 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -182,7 +182,7 @@ void Game::handleTile(char tile) { std::cout << "Enemy encountered. Combat will be added later.\n"; } else if (tile == 'N') { - std::cout << "NPC: Hello, traveler.\n"; + interactWithNPC(*location); } else if (tile == 'D') { if (!location->riddle.isSolved) { @@ -314,4 +314,128 @@ void Game::openChest(Location& location) { } std::cout << "Chest not found. Something went wrong.\n"; +} + + +void Game::applyDialogResult(NPC& npc, const DialogOption& option) { + if (option.result == DialogResultType::GiveItem) { + player.inventory.addItem(option.value); + std::cout << "Received item ID: " << option.value << "\n"; + } + else if (option.result == DialogResultType::BecomeHostile) { + npc.state = NPCState::Hostile; + startCombat(npc); + } + else if (option.result == DialogResultType::OpenLocation) { + for (auto& location : locations) { + if (location.id == option.value) { + location.riddle.isSolved = true; + std::cout << "Location " << option.value << " has been unlocked.\n"; + return; + } + } + + std::cout << "Location " << option.value << " not found.\n"; + } + else if (option.result == DialogResultType::Fight) { + startCombat(npc); + } + else { + std::cout << "Nothing happened.\n"; + } +} + +void Game::startCombat(NPC& npc) { + std::cout << "\nCombat started with " << npc.name << "!\n"; + + while (player.hp > 0 && npc.hp > 0) { + std::cout << "\nYour HP: " << player.hp << "\n"; + std::cout << npc.name << " HP: " << npc.hp << "\n"; + + std::cout << "1. Attack\n"; + std::cout << "2. Use small potion 101\n"; + std::cout << "3. Run\n"; + std::cout << "> "; + + int choice; + std::cin >> choice; + + if (choice == 1) { + int damage = player.getDamage(itemDatabase); + npc.hp -= damage; + + std::cout << "You dealt " << damage << " damage.\n"; + + if (npc.hp <= 0) { + npc.defeated = true; + std::cout << "You defeated " << npc.name << ".\n"; + return; + } + } + else if (choice == 2) { + player.usePotion(101, itemDatabase); + } + else if (choice == 3) { + std::cout << "You ran away.\n"; + return; + } + else { + std::cout << "Invalid action.\n"; + continue; + } + + player.hp -= npc.damage; + std::cout << npc.name << " dealt " << npc.damage << " damage.\n"; + + if (player.hp <= 0) { + std::cout << "You died.\n"; + isRunning = false; + return; + } + } +} + +void Game::interactWithNPC(Location& location) { + for (auto& npc : location.npcs) { + if (npc.position.x == player.position.x && npc.position.y == player.position.y) { + if (npc.defeated) { + std::cout << "There is nobody useful here anymore.\n"; + return; + } + + std::cout << "\n=== " << npc.name << " ===\n"; + + if (npc.state == NPCState::Hostile) { + std::cout << npc.name << " is hostile.\n"; + startCombat(npc); + return; + } + + std::cout << npc.dialogText << "\n"; + + for (int i = 0; i < npc.options.size(); i++) { + std::cout << i + 1 << ". " << npc.options[i].text << "\n"; + } + + std::cout << "> "; + + int choice; + std::cin >> choice; + + if (choice < 1 || choice > npc.options.size()) { + std::cout << "Invalid dialog option.\n"; + return; + } + + const DialogOption& option = npc.options[choice - 1]; + + std::cout << option.message << "\n"; + + applyDialogResult(npc, option); + + return; + } + } + + std::cout << "NPC not found.\n"; } \ No newline at end of file diff --git a/src/location.cpp b/src/location.cpp index f8b8ed7..a67eadb 100644 --- a/src/location.cpp +++ b/src/location.cpp @@ -64,7 +64,23 @@ bool Location::loadFromFile(const std::string& path) { if (tile == 'C') { Chest chest; chest.pos = { x, y }; - chest.itemIds = { 101, 102 }; + + if (id == 1) { + chest.itemIds = { 101, 101 }; + } + else if (id == 2) { + chest.itemIds = { 101, 201 }; + } + else if (id == 3) { + chest.itemIds = { 102 }; + } + else if (id == 4) { + chest.itemIds = { 102, 202 }; + } + else if (id == 5) { + chest.itemIds = { 101, 102, 202 }; + } + chests.push_back(chest); } else if (tile == 'E') { @@ -76,9 +92,26 @@ bool Location::loadFromFile(const std::string& path) { } else if (tile == 'N') { NPC npc; - npc.pos = { x, y }; - npc.name = "Stranger"; - npcs.push_back(npc); + + std::string npcPath; + + if (id == 1) { + npcPath = "data/npcs/npc_1.txt"; + } + else if (id == 2) { + npcPath = "data/npcs/npc_2.txt"; + } + else if (id == 3) { + npcPath = "data/npcs/npc_3.txt"; + } + else { + npcPath = "data/npcs/npc_1.txt"; + } + + if (npc.loadFromFile(npcPath)) { + npc.position = { x, y }; + npcs.push_back(npc); + } } } } diff --git a/src/npc.cpp b/src/npc.cpp new file mode 100644 index 0000000..a296b65 --- /dev/null +++ b/src/npc.cpp @@ -0,0 +1,81 @@ +#include "../include/NPC.h" +#include +#include + +NPCState NPC::parseState(const std::string& value) { + if (value == "FRIENDLY") return NPCState::Friendly; + if (value == "HOSTILE") return NPCState::Hostile; + + return NPCState::Friendly; +} + +DialogResultType NPC::parseDialogResult(const std::string& value) { + if (value == "GIVE_ITEM") return DialogResultType::GiveItem; + if (value == "BECOME_HOSTILE") return DialogResultType::BecomeHostile; + if (value == "OPEN_LOCATION") return DialogResultType::OpenLocation; + if (value == "FIGHT") return DialogResultType::Fight; + + return DialogResultType::Nothing; +} + +bool NPC::loadFromFile(const std::string& path) { + std::ifstream file(path); + + if (!file.is_open()) { + std::cout << "Cannot open NPC file: " << path << "\n"; + return false; + } + + std::string line; + + DialogOption option1; + DialogOption option2; + DialogOption option3; + + while (std::getline(file, line)) { + if (line.empty()) { + continue; + } + + if (line == "END") { + break; + } + + size_t pos = line.find('='); + + if (pos == std::string::npos) { + continue; + } + + std::string key = line.substr(0, pos); + std::string value = line.substr(pos + 1); + + if (key == "ID") id = std::stoi(value); + else if (key == "NAME") name = value; + else if (key == "STATE") state = parseState(value); + else if (key == "HP") hp = std::stoi(value); + else if (key == "DAMAGE") damage = std::stoi(value); + else if (key == "DIALOG_TEXT") dialogText = value; + + else if (key == "OPTION_1") option1.text = value; + else if (key == "RESULT_1") option1.result = parseDialogResult(value); + else if (key == "VALUE_1") option1.value = std::stoi(value); + else if (key == "MESSAGE_1") option1.message = value; + + else if (key == "OPTION_2") option2.text = value; + else if (key == "RESULT_2") option2.result = parseDialogResult(value); + else if (key == "VALUE_2") option2.value = std::stoi(value); + else if (key == "MESSAGE_2") option2.message = value; + + else if (key == "OPTION_3") option3.text = value; + else if (key == "RESULT_3") option3.result = parseDialogResult(value); + else if (key == "VALUE_3") option3.value = std::stoi(value); + else if (key == "MESSAGE_3") option3.message = value; + } + + if (!option1.text.empty()) options.push_back(option1); + if (!option2.text.empty()) options.push_back(option2); + if (!option3.text.empty()) options.push_back(option3); + + return true; +} \ No newline at end of file