From 795a43dd8924ca78a9d42e60460cccd736b833cf Mon Sep 17 00:00:00 2001 From: obligaron Date: Sat, 11 Jun 2022 20:25:57 +0200 Subject: [PATCH] Add Arena Support --- CMake/Assets.cmake | 3 + Packaging/resources/assets/arena/church.dun | Bin 0 -> 20404 bytes .../assets/arena/circle_of_death.dun | Bin 0 -> 30604 bytes Packaging/resources/assets/arena/hell.dun | Bin 0 -> 33324 bytes Source/control.cpp | 104 ++++++++++++++++++ Source/interfac.cpp | 47 ++++---- Source/levels/gendung.h | 19 +++- Source/levels/setmaps.cpp | 66 +++++++---- Source/levels/trigs.cpp | 41 +++++++ Source/missiles.cpp | 3 + Source/player.cpp | 2 +- Source/player.h | 5 + Source/quests.cpp | 7 ++ 13 files changed, 256 insertions(+), 41 deletions(-) create mode 100644 Packaging/resources/assets/arena/church.dun create mode 100644 Packaging/resources/assets/arena/circle_of_death.dun create mode 100644 Packaging/resources/assets/arena/hell.dun 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 0000000000000000000000000000000000000000..9c52ffc1ff77810bab53ae24d63de4cc9a5c0a0f GIT binary patch literal 20404 zcmeH_TTa6;5Jjg_1^iT0YH2D}AoxK1um@fX0I>=rURz)VNURE$g&or-&do$o=?AE% zRulWq-0|etQ#-QR8sos`<+!%xtZ1!L*w9Yx!Y=K~cEhX)qiw&!7Pe<^c4MRPJnzVR zE%QcY-d;!E&W7_Ab{zIh1l#$d2z+H)mMb zy}j6-orM3>&pWP*kX_F#?0NMC%P)!V*h6(DaemvH_nFm;C!2_Tl-cPS>wRYRVi&32 zyX}W)dZxU3e{ue}+V^2KYu2m!(|zmps{VA}`uA4-)tR*S_gw3$%I?cAKabdJJk76i zENAm@-qVax6YcmtRb)|0(#qgrO%$eTO-s{0!qnO)UAr|)`+@QQJ1f%TNU4}5p}Dg zE?-f%D!yAI>Q+TvzM^hbe78o_t%|yQMcu0SZjGp06?OTFx>fPr8d0|@>hcwJtKz#g fqHa~x>BBAb}NJyZli?(@^R_Yt{XV?GsG1B(E>xsEwd}kcb;G_tBSdsC; zXU^Pju2EK8%e6e?Xb7xid30<8R2DLq*ZX5Di;#CFSNmfT_P&r;^7qak_r8=r@>`xa z?zs1qp7Y*KXGUw;$k-{ZWL?cOFbt5VnApLOnGP9<0Gxi42;pX7^t-zAUFeJAf5&u952Kbv!V zRn{{u_pAJnU*X*DI?MZ5?v)(RsGXnRbD6I-qjrA2x6HS6dN>cychl?(&Fwxzhwr@g zZr|9s?abA^+W##(@Ac{O_D0z+ZNB!Pcem&E{r%p#@f4LOOL>$G>uj&LU3u3jydVGp z2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$## zAOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2teSG2;4qWPCsVZ7ar+) zs`WNTX z+WSzSLpl9*57p`4tJl#!ts0p+(LTN0Xr1$2@3<*n&U(Fx9_y_4m`DHWeD`y|Q~NqL z-y6BH%Y@abxJ83w>y1|T?`OR}n<+o}AM@3x;vO0SG_<0+&W$ z*8KLgbv*PstxVM(`bBO_0M64AU|+GXrrl9ggTUnw@V*~yeY$nq^*xo`ECG+Gt!Gne zpIzTmYkyYp5^O!2Qv2-so?82}ikD#P*_7I6*Z0)gpH;jBThFG{KD)lB*8Z&GCD?j4 zrS{qNJ+<~{6)(ZovnjRDuJ5U}KdX2Nww_I?eRh3Mt^HZWOR)89O6{}jdur{^Dqe!E bXH#mQUEfn{e^&7lY(1NT4+0Q4e**sixeQ?l literal 0 HcmV?d00001 diff --git a/Packaging/resources/assets/arena/hell.dun b/Packaging/resources/assets/arena/hell.dun new file mode 100644 index 0000000000000000000000000000000000000000..8e7666d5bde136f398a2fb7e5e612bd4b1265940 GIT binary patch literal 33324 zcmeI5%}x|i7)0;5AYo?)O-Pu9iHW=bB)Zq7(S<)x;)X;Qi0^Ip3_bMl&7rE5F$@+R zGElef>HbdBz0^f}*`Dp~@w^>v91C-jdHc}bt&c^U+xyhe>wcZA&)&CI-4%JZyq0;FJ-Orf zv23lnoBL+#yFJ((f3D0vj(vyiO*iZ7_H;C{Gv9gp)V_3k&L{3WXrJ5H(VpI`bH20d zJ;SHF@BQ_BA4lhO&Nsf_!_WWkzMCg)^Ub^Z?HHb;o43QZU47O+(e*jnd^^x@<9#RM zNguxj7R$2^)Vn7eU-#{|&p_DqXUkU%#6S$hKn%n{48%YT#6S$hKn%n{48%YT#6S$h zKn%n{48%YT#6S$hKn%n{48%YT#6S$hKn%n{48%YT#6S$hKn%n{48%YT#6S$hKn%n{ z48%YT#6S$hKn%n{48%YT#6S$hKn(1mfzx)n^{Oe~T7J;sC0rflP`BeFXi^S!JEl1( zx4}@PIVcB=9n&0?+h8ct9Fzmbj%g0cZ7>vR4$1*z$214!HW-RD2jzgVW153<8w^F7 zgL1&wG0j1_4Td7kK{;UTnC76|21AkNpd2uEOmk3fgP}-sP!1S7ra36L!BC_*CYCp<$$qcnuBs13`LrQa=_Rz%|W>h zh9b>DIbiIV=AhgLLy_j7958lFb5L%Bp-6L34j4P8IViWmP^3912aFxl9F*JOJt$sw z{|fZ|e(%B1xx^TVff$H^7(fHF?tezue&CkBTG)QBKHbe%&Ue3F<1;>|7Bk z8w0yHz)zfB`ID>DRBq&a)d1VlE0v}bP34BB*9Oz{N~P&UQ@Nq(wZSyKQfWHTRBmW` zZ7@x*RGLmSl^dE~8%)zHm8KI-<%Xu$2GjIPrRhXdxuNN`!8E;6X*$tVZfJUKFio#i znocy88=77lOw%isrV~x&hNjmB)AUNE=|oexq3N~3G`&)3I?+^aXnJihO|Mj%PBfJp cnqC`B(<_yx6HVoYrl%1DF%SbW@URX12E6%c)c^nh literal 0 HcmV?d00001 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; } }