diff --git a/CMake/Assets.cmake b/CMake/Assets.cmake index b1c9b1441..5033dea10 100644 --- a/CMake/Assets.cmake +++ b/CMake/Assets.cmake @@ -41,6 +41,9 @@ if (Gettext_FOUND) endif() set(devilutionx_assets + arena/church.dun + arena/circle_of_death.dun + arena/hell.dun data/boxleftend.clx data/boxmiddle.clx data/boxrightend.clx diff --git a/Packaging/resources/assets/arena/church.dun b/Packaging/resources/assets/arena/church.dun new file mode 100644 index 000000000..9c52ffc1f Binary files /dev/null and b/Packaging/resources/assets/arena/church.dun differ diff --git a/Packaging/resources/assets/arena/circle_of_death.dun b/Packaging/resources/assets/arena/circle_of_death.dun new file mode 100644 index 000000000..d16b07f87 Binary files /dev/null and b/Packaging/resources/assets/arena/circle_of_death.dun differ diff --git a/Packaging/resources/assets/arena/hell.dun b/Packaging/resources/assets/arena/hell.dun new file mode 100644 index 000000000..8e7666d5b Binary files /dev/null and b/Packaging/resources/assets/arena/hell.dun differ diff --git a/Source/control.cpp b/Source/control.cpp index 8b869de54..77b0f27ab 100644 --- a/Source/control.cpp +++ b/Source/control.cpp @@ -26,6 +26,7 @@ #include "init.h" #include "inv.h" #include "inv_iterators.hpp" +#include "levels/setmaps.h" #include "levels/trigs.h" #include "lighting.h" #include "minitext.h" @@ -317,12 +318,113 @@ int DrawDurIcon4Item(const Surface &out, Item &pItem, int x, int c) return x - 32 - 8; } +struct TextCmdItem { + const std::string text; + const std::string description; + const std::string requiredParameter; + std::string (*actionProc)(const string_view); +}; + +extern std::vector TextCmdList; + +std::string TextCmdHelp(const string_view parameter) +{ + if (parameter.empty()) { + std::string ret; + StrAppend(ret, _("Available Commands:")); + for (const TextCmdItem &textCmd : TextCmdList) { + StrAppend(ret, " ", _(textCmd.text)); + } + return ret; + } + auto textCmdIterator = std::find_if(TextCmdList.begin(), TextCmdList.end(), [&](const TextCmdItem &elem) { return elem.text == parameter; }); + if (textCmdIterator == TextCmdList.end()) + return StrCat(_("Command "), parameter, _(" is unkown.")); + auto &textCmdItem = *textCmdIterator; + if (textCmdItem.requiredParameter.empty()) + return StrCat(_("Description: "), _(textCmdItem.description), _("\nParameters: No additional parameter needed.")); + return StrCat(_("Description: "), _(textCmdItem.description), _("\nParameters: "), _(textCmdItem.requiredParameter)); +} + +void AppendArenaOverview(std::string &ret) +{ + for (int arena = SL_FIRST_ARENA; arena <= SL_LAST; arena++) { + StrAppend(ret, "\n", arena - SL_FIRST_ARENA + 1, " (", QuestLevelNames[arena], ")"); + } +} + +const dungeon_type DungeonTypeForArena[] = { + dungeon_type::DTYPE_CATHEDRAL, // SL_ARENA_CHURCH + dungeon_type::DTYPE_HELL, // SL_ARENA_HELL + dungeon_type::DTYPE_HELL, // SL_ARENA_CIRCLE_OF_LIFE +}; + +std::string TextCmdArena(const string_view parameter) +{ + std::string ret; + if (!gbIsMultiplayer) { + StrAppend(ret, _("Arenas are only supported in multiplayer.")); + return ret; + } + + if (parameter.empty()) { + StrAppend(ret, _("What arena do you want to visit?")); + AppendArenaOverview(ret); + return ret; + } + + int arenaNumber = atoi(parameter.data()); + _setlevels arenaLevel = static_cast<_setlevels>(arenaNumber - 1 + SL_FIRST_ARENA); + if (arenaNumber < 0 || !IsArenaLevel(arenaLevel)) { + StrAppend(ret, _("Invalid arena-number. Valid numbers are:")); + AppendArenaOverview(ret); + return ret; + } + + if (!MyPlayer->isOnLevel(0) && !MyPlayer->isOnArenaLevel()) { + StrAppend(ret, _("To enter a arena, you need to be in town or another arena.")); + return ret; + } + + setlvltype = DungeonTypeForArena[arenaLevel - SL_FIRST_ARENA]; + StartNewLvl(*MyPlayer, WM_DIABSETLVL, arenaLevel); + return ret; +} + +std::vector TextCmdList = { + { N_("/help"), N_("Prints help overview or help for a specific command."), N_("({command})"), &TextCmdHelp }, + { N_("/arena"), N_("Enter a PvP Arena."), N_("{arena-number}"), &TextCmdArena } +}; + +bool CheckTextCommand(const string_view text) +{ + if (text.size() < 1 || text[0] != '/') + return false; + + auto textCmdIterator = std::find_if(TextCmdList.begin(), TextCmdList.end(), [&](const TextCmdItem &elem) { return text.find(elem.text) == 0 && (text.length() == elem.text.length() || text[elem.text.length()] == ' '); }); + if (textCmdIterator == TextCmdList.end()) { + InitDiabloMsg(StrCat(_("Command \""), text, "\" is unknown.")); + return true; + } + + TextCmdItem &textCmd = *textCmdIterator; + string_view parameter = ""; + if (text.length() > (textCmd.text.length() + 1)) + parameter = text.substr(textCmd.text.length() + 1); + const std::string result = textCmd.actionProc(parameter); + if (result != "") + InitDiabloMsg(result); + return true; +} + void ResetTalkMsg() { #ifdef _DEBUG if (CheckDebugTextCommand(TalkMessage)) return; #endif + if (CheckTextCommand(TalkMessage)) + return; uint32_t pmask = 0; @@ -1294,6 +1396,8 @@ void DiabloHotkeyMsg(uint32_t dwMsg) if (CheckDebugTextCommand(msg)) continue; #endif + if (CheckTextCommand(msg)) + continue; char charMsg[MAX_SEND_STR_LEN]; CopyUtf8(charMsg, msg, sizeof(charMsg)); NetSendCmdString(0xFFFFFF, charMsg); diff --git a/Source/interfac.cpp b/Source/interfac.cpp index 0c4c1c701..ff003720e 100644 --- a/Source/interfac.cpp +++ b/Source/interfac.cpp @@ -48,6 +48,28 @@ OptionalOwnedClxSpriteList ArtCutsceneWidescreen; uint32_t CustomEventsBegin = SDL_USEREVENT; constexpr uint32_t NumCustomEvents = WM_LAST - WM_FIRST + 1; +Cutscenes GetCutSceneFromLevelType(dungeon_type type) +{ + switch (type) { + case DTYPE_TOWN: + return CutTown; + case DTYPE_CATHEDRAL: + return CutLevel1; + case DTYPE_CATACOMBS: + return CutLevel2; + case DTYPE_CAVES: + return CutLevel3; + case DTYPE_HELL: + return CutLevel4; + case DTYPE_NEST: + return CutLevel6; + case DTYPE_CRYPT: + return CutLevel5; + default: + return CutLevel1; + } +} + Cutscenes PickCutscene(interface_mode uMsg) { switch (uMsg) { @@ -65,25 +87,7 @@ Cutscenes PickCutscene(interface_mode uMsg) return CutTown; if (lvl == 16 && uMsg == WM_DIABNEXTLVL) return CutGate; - - switch (GetLevelType(lvl)) { - case DTYPE_TOWN: - return CutTown; - case DTYPE_CATHEDRAL: - return CutLevel1; - case DTYPE_CATACOMBS: - return CutLevel2; - case DTYPE_CAVES: - return CutLevel3; - case DTYPE_HELL: - return CutLevel4; - case DTYPE_NEST: - return CutLevel6; - case DTYPE_CRYPT: - return CutLevel5; - default: - return CutLevel1; - } + return GetCutSceneFromLevelType(GetLevelType(lvl)); } case WM_DIABWARPLVL: return CutPortal; @@ -93,6 +97,11 @@ Cutscenes PickCutscene(interface_mode uMsg) return CutLevel2; if (setlvlnum == SL_VILEBETRAYER) return CutPortalRed; + if (IsArenaLevel(setlvlnum)) { + if (uMsg == WM_DIABSETLVL) + return GetCutSceneFromLevelType(setlvltype); + return CutTown; + } return CutLevel1; default: app_fatal("Unknown progress mode"); diff --git a/Source/levels/gendung.h b/Source/levels/gendung.h index f3bdac79f..ad115efda 100644 --- a/Source/levels/gendung.h +++ b/Source/levels/gendung.h @@ -37,9 +37,26 @@ enum _setlevels : int8_t { SL_POISONWATER, SL_VILEBETRAYER, - SL_LAST = SL_VILEBETRAYER, + SL_ARENA_CHURCH, + SL_ARENA_HELL, + SL_ARENA_CIRCLE_OF_LIFE, + + SL_FIRST_ARENA = SL_ARENA_CHURCH, + SL_LAST = SL_ARENA_CIRCLE_OF_LIFE, }; +inline bool IsArenaLevel(_setlevels setLevel) +{ + switch (setLevel) { + case SL_ARENA_CHURCH: + case SL_ARENA_HELL: + case SL_ARENA_CIRCLE_OF_LIFE: + return true; + default: + return false; + } +} + enum dungeon_type : int8_t { DTYPE_TOWN, DTYPE_CATHEDRAL, diff --git a/Source/levels/setmaps.cpp b/Source/levels/setmaps.cpp index b622dc772..f3ab23384 100644 --- a/Source/levels/setmaps.cpp +++ b/Source/levels/setmaps.cpp @@ -26,6 +26,9 @@ const char *const QuestLevelNames[] = { N_("Maze"), N_("Poisoned Water Supply"), N_("Archbishop Lazarus' Lair"), + N_("Church Arena"), + N_("Hell Arena"), + N_("Circle of Life Arena"), }; namespace { @@ -64,6 +67,39 @@ void SetMapTransparency(const char *path) LoadTransparency(dunData.get()); } +void LoadCustomMap(const char *path, Point viewPosition) +{ + switch (setlvltype) { + case DTYPE_CATHEDRAL: + case DTYPE_CRYPT: + LoadL1Dungeon(path, viewPosition); + break; + case DTYPE_CATACOMBS: + LoadL2Dungeon(path, viewPosition); + break; + case DTYPE_CAVES: + case DTYPE_NEST: + LoadL3Dungeon(path, viewPosition); + break; + case DTYPE_HELL: + LoadL4Dungeon(path, viewPosition); + break; + case DTYPE_TOWN: + case DTYPE_NONE: + break; + } + LoadRndLvlPal(setlvltype); +} + +void LoadArenaMap(const char *path, Point viewPosition, Point exitTrigger) +{ + LoadCustomMap(path, viewPosition); + trigflag = false; + numtrigs = 1; + trigs[0].position = exitTrigger; + trigs[0]._tmsg = WM_DIABRTNLVL; +} + } // namespace void LoadSetMap() @@ -111,28 +147,18 @@ void LoadSetMap() AddVileObjs(); InitNoTriggers(); break; + case SL_ARENA_CHURCH: + LoadArenaMap("arena\\church.dun", { 37, 22 }, { 36, 20 }); + break; + case SL_ARENA_HELL: + LoadArenaMap("arena\\hell.dun", { 44, 32 }, { 43, 32 }); + break; + case SL_ARENA_CIRCLE_OF_LIFE: + LoadArenaMap("arena\\circle_of_death.dun", { 48, 34 }, { 47, 34 }); + break; case SL_NONE: #ifdef _DEBUG - switch (setlvltype) { - case DTYPE_CATHEDRAL: - case DTYPE_CRYPT: - LoadL1Dungeon(TestMapPath.c_str(), ViewPosition); - break; - case DTYPE_CATACOMBS: - LoadL2Dungeon(TestMapPath.c_str(), ViewPosition); - break; - case DTYPE_CAVES: - case DTYPE_NEST: - LoadL3Dungeon(TestMapPath.c_str(), ViewPosition); - break; - case DTYPE_HELL: - LoadL4Dungeon(TestMapPath.c_str(), ViewPosition); - break; - case DTYPE_TOWN: - case DTYPE_NONE: - break; - } - LoadRndLvlPal(setlvltype); + LoadCustomMap(TestMapPath.c_str(), ViewPosition); InitNoTriggers(); #endif break; diff --git a/Source/levels/trigs.cpp b/Source/levels/trigs.cpp index b1cebf83f..0303f93eb 100644 --- a/Source/levels/trigs.cpp +++ b/Source/levels/trigs.cpp @@ -758,6 +758,45 @@ bool ForcePWaterTrig() return false; } +bool ForceArenaTrig() +{ + int *checkList = nullptr; + switch (setlvltype) { + case DTYPE_TOWN: + checkList = TownWarp1List; + break; + case DTYPE_CATHEDRAL: + checkList = L1UpList; + break; + case DTYPE_CATACOMBS: + checkList = L2TWarpUpList; + break; + case DTYPE_CAVES: + checkList = L3TWarpUpList; + break; + case DTYPE_HELL: + checkList = L4TWarpUpList; + break; + case DTYPE_NEST: + checkList = L5TWarpUpList; + break; + case DTYPE_CRYPT: + checkList = L6TWarpUpList; + break; + default: + return false; + } + for (int i = 0; checkList[i] != -1; i++) { + if (dPiece[cursPosition.x][cursPosition.y] == checkList[i]) { + InfoString = _("Up to town"); + cursPosition = trigs[0].position; + return true; + } + } + + return false; +} + void CheckTrigForce() { trigflag = false; @@ -807,6 +846,8 @@ void CheckTrigForce() trigflag = ForcePWaterTrig(); break; default: + if (IsArenaLevel(setlvlnum)) + trigflag = ForceArenaTrig(); break; } } diff --git a/Source/missiles.cpp b/Source/missiles.cpp index 7023f51b3..a23b6485c 100644 --- a/Source/missiles.cpp +++ b/Source/missiles.cpp @@ -273,6 +273,9 @@ bool Plr2PlrMHit(const Player &player, int p, int mindam, int maxdam, int dist, *blocked = false; + if (target.isOnArenaLevel() && target._pmode == PM_WALK_SIDEWAYS) + return false; + if (target._pInvincible) { return false; } diff --git a/Source/player.cpp b/Source/player.cpp index fd37c7dbc..638ba064b 100644 --- a/Source/player.cpp +++ b/Source/player.cpp @@ -2949,7 +2949,7 @@ StartPlayerKill(Player &player, int earflag) NetSendCmdParam1(true, CMD_PLRDEAD, earflag); } - bool diablolevel = gbIsMultiplayer && player.plrlevel == 16; + bool diablolevel = gbIsMultiplayer && (player.isOnLevel(16) || player.isOnArenaLevel()); player.Say(HeroSpeech::AuughUh); diff --git a/Source/player.h b/Source/player.h index b68cfdab6..23aef22ce 100644 --- a/Source/player.h +++ b/Source/player.h @@ -742,6 +742,11 @@ struct Player { { return this->plrIsOnSetLevel && this->plrlevel == static_cast(level); } + /** @brief Checks if the player is on a arena level. */ + bool isOnArenaLevel() const + { + return plrIsOnSetLevel && IsArenaLevel(static_cast<_setlevels>(plrlevel)); + } void setLevel(uint8_t level) { this->plrlevel = level; diff --git a/Source/quests.cpp b/Source/quests.cpp index f8784894b..cf5566b0f 100644 --- a/Source/quests.cpp +++ b/Source/quests.cpp @@ -514,6 +514,13 @@ void SetReturnLvlPos() break; case SL_NONE: break; + default: + if (IsArenaLevel(setlvlnum)) { + ReturnLvlPosition = Towners[TOWN_DRUNK].position + Displacement { 1, 0 }; + ReturnLevel = 0; + ReturnLevelType = DTYPE_TOWN; + } + break; } }