diff --git a/README.md b/README.md index 09e7a90..8ebb5bd 100644 --- a/README.md +++ b/README.md @@ -23,15 +23,26 @@ It builds as it is, without external dependencies. ## ๐Ÿ“‹ Changelog ### ๐Ÿ› Bug Fixes -* **HP/SP Value Persistence:** Fixed an issue where current HP/SP values were incorrectly affected by changes to Max HP/SP (such as stat upgrades or equipment changes). -* **GM Kill Command:** Updated the `/kill` command to properly zero out the target's HP. -* **Messenger Deletion (Offline):** Fixed an issue where a companion needed to be online for a "remove from messenger" action to complete. -* **Messenger Deletion (Cross-Channel):** Fixed an issue where the removal message was visible to both parties even if they were in different channels or cores at the time of deletion. -* **Dungeon Party Logic:** Fixed an issue where the same message would popup to all affected parties when a leader tried to kick a player or a player tried to leave a team while inside a dungeon. Includes several other update message optimizations and dungeon logic improvements regarding party kicking/leaving. +* **Horse skill grade check:** Fixed a check that required the horse skill's grade to be exactly 3 (Grand Master) in order to use skills, leaving GMs that set their skill to P unable to use riding skills +* **Quest state loading:** Fixed an issue where quest states (`when login begin`) was being loaded before character affects on `PHASE_GAME` +* **Sura's Flame Ghost mounting:** Fixed Sura's Flame Ghost when mounting. The skill now damages nearby enemies when the horse skill is more than Level 10. +* **Experience points from chests:** Fixed a bug where experience points were being replaced by Experience Rings when opening chests +* **Gold from chests:** Fixed a bug where Gold was being replaced by a Gold inventory item that had no value when opening chests ### โฌ†๏ธ Feature Improvements -* **Job-Specific Stat Resets:** Individual stats reset scrolls (Items 71103, 71104, 71105, 71106) now recover stats to their initial values based on character job instead of defaulting to 1, returning the appropriate points. Translations now dynamically display the selected stat's value. Translation for Reset all-status scroll (71002) also adjusted from printing 'to 1'. -* **Logout Interruption:** Using a skill now automatically cancels the logout countdown for both the user and the target (if applicable). -* **Auto Potion Logic:** Auto potions can now be moved within the inventory while active and are automatically disabled immediately before being dropped to the ground. +* **Nemere's Watchtower dungeon safeguards:** Added checks for automatically dismounting/prevent mounting of any kind when the character is inside the new Nemere's Watchtower dungeon +* **Conditional damage immunity system:** Added various checks for true per-hit conditional damage immunity with full control via Lua functions as well as clones of the default mob/group spawning Lua functions that return the monsters' VIDs for further manipulation from the quests. Exposed Lua functions below: + * `d.regen_file_with_immunity`: Spawn all monster/groups from a dungeon folder's regen.txt file with conditional immunity embedded from spawn + * `d.regen_file_with_vids`: The VIDs of all spawned monsters/groups from a dungeon folder's regen.txt file are being returned to Lua for further manipulation + * `d.spawn_group_with_immunity`: Spawn a group of monsters via its ID with conditional immunity embedded from spawn + * `d.spawn_group_with_vids`: The VIDs of all monsters from the group spawned are being returned to Lua for further manipulation + * `d.spawn_mob_with_immunity`: Spawn a single monster with conditional immunity embedded from spawn + * `npc.add_damage_immunity_condition`: Add a damage immunity condition to an already spawned monster + * `npc.clear_damage_immunity_conditions`: Clear all damage immunity from a monster so it can take damage normally again + * `npc.is_damage_immune`: Check if a mob has damage immunity using its VID + * `npc.set_damage_immunity`: Set damage immunity to a monster using its VID + * `npc.set_damage_immunity_with_conditions`: Set conditional damage immunity to a monster using its VID + * **Immunity vs Conditional immunity**: When a monster is immune to damage all hits are returning as MISS and it cannot be poisoned, burned, slowed or stunned. A condition is a rule that when applied, the monster's immunity is being ignored (for example, a mob is immune to damage unless the attacker is a Ninja - job 1). Multiple conditions are possible. + * More about the available conditions for damage immunity in `game/char.h` diff --git a/src/db/ClientManagerPlayer.cpp b/src/db/ClientManagerPlayer.cpp index 06e4e10..2dcffd6 100644 --- a/src/db/ClientManagerPlayer.cpp +++ b/src/db/ClientManagerPlayer.cpp @@ -306,13 +306,13 @@ void CClientManager::QUERY_PLAYER_LOAD(CPeer * peer, DWORD dwHandle, TPlayerLoad "SELECT dwPID,szName,szState,lValue FROM quest%s WHERE dwPID=%d AND lValue<>0", GetTablePostfix(), pTab->id); - CDBManager::instance().ReturnQuery(szQuery, QID_QUEST, peer->GetHandle(), new ClientHandleInfo(dwHandle,0,packet->account_id)); + CDBManager::instance().ReturnQuery(szQuery, QID_QUEST, peer->GetHandle(), new ClientHandleInfo(dwHandle, 0, packet->account_id)); // Affect snprintf(szQuery, sizeof(szQuery), "SELECT dwPID,bType,bApplyOn,lApplyValue,dwFlag,lDuration,lSPCost FROM affect%s WHERE dwPID=%d", GetTablePostfix(), pTab->id); - CDBManager::instance().ReturnQuery(szQuery, QID_AFFECT, peer->GetHandle(), new ClientHandleInfo(dwHandle)); + CDBManager::instance().ReturnQuery(szQuery, QID_AFFECT, peer->GetHandle(), new ClientHandleInfo(dwHandle, packet->player_id)); } ///////////////////////////////////////////// // 2) ์•„์ดํ…œ์ด DBCache ์— ์—†์Œ : DB ์—์„œ ๊ฐ€์ ธ์˜ด @@ -601,6 +601,22 @@ void CClientManager::RESULT_COMPOSITE_PLAYER(CPeer * peer, SQLMsg * pMsg, DWORD case QID_AFFECT: sys_log(0, "QID_AFFECT %u", info->dwHandle); + + // MR-8: Fix "when_login" being loaded before character affects + if (!mysql_num_rows(pSQLResult)) + { + TPacketAffectElement pAffElem{}; + DWORD dwCount = 0; + + peer->EncodeHeader(HEADER_DG_AFFECT_LOAD, info->dwHandle, sizeof(DWORD) + sizeof(DWORD) + sizeof(TPacketAffectElement) * dwCount); + peer->Encode(&info->player_id, sizeof(DWORD)); + peer->Encode(&dwCount, sizeof(DWORD)); + peer->Encode(&pAffElem, sizeof(TPacketAffectElement) * dwCount); + + break; + } + // MR-8: -- END OF -- Fix "when_login" being loaded before character affects + RESULT_AFFECT_LOAD(peer, pSQLResult, info->dwHandle); break; /* diff --git a/src/game/BlueDragon_Skill.h b/src/game/BlueDragon_Skill.h index 95f1ddc..84d5d08 100644 --- a/src/game/BlueDragon_Skill.h +++ b/src/game/BlueDragon_Skill.h @@ -272,7 +272,9 @@ struct FSkillEarthQuake ch->Damage( pAttacker, dam, DAMAGE_TYPE_ICE); - SkillAttackAffect( ch, 1000, IMMUNE_STUN, AFFECT_STUN, POINT_NONE, 0, AFF_STUN, sec, "BDRAGON_STUN" ); + // MR-8: Snow dungeon - All-damage immunity with exceptions + SkillAttackAffect( pAttacker, ch, 1000, IMMUNE_STUN, AFFECT_STUN, POINT_NONE, 0, AFF_STUN, sec, "BDRAGON_STUN" ); + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions sys_log(0, "BlueDragon: EarthQuake to %s addPct(%d) dam(%d) sec(%d)", ch->GetName(), addPct, dam, sec); diff --git a/src/game/battle.h b/src/game/battle.h index 6830bf3..0537af5 100644 --- a/src/game/battle.h +++ b/src/game/battle.h @@ -40,6 +40,16 @@ inline void AttackAffect(LPCHARACTER pkAttacker, int time, const char* name) { + // MR-8: Snow dungeon - All-damage immunity with exceptions + if (pkVictim->IsDamageImmune() && (pkVictim->IsMonster() || pkVictim->IsStone() || pkVictim->IsDoor())) + { + if (!pkVictim->CheckDamageImmunityConditions(pkAttacker)) + { + return; // Immunity prevents stun application + } + } + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions + if (pkAttacker->GetPoint(att_point) && !pkVictim->IsAffectFlag(affect_flag)) { if (number(1, 100) <= pkAttacker->GetPoint(att_point) && !pkVictim->IsImmune(immune_flag)) @@ -58,7 +68,9 @@ inline void AttackAffect(LPCHARACTER pkAttacker, } } -inline void SkillAttackAffect(LPCHARACTER pkVictim, +// MR-8: Snow dungeon - All-damage immunity with exceptions +inline void SkillAttackAffect(LPCHARACTER pkAttacker, + LPCHARACTER pkVictim, int success_pct, DWORD immune_flag, DWORD affect_idx, @@ -68,6 +80,15 @@ inline void SkillAttackAffect(LPCHARACTER pkVictim, int time, const char* name) { + if (pkVictim->IsDamageImmune() && (pkVictim->IsMonster() || pkVictim->IsStone() || pkVictim->IsDoor())) + { + if (!pkVictim->CheckDamageImmunityConditions(pkAttacker)) + { + return; // Immunity prevents skill affect application + } + } + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions + if (success_pct && !pkVictim->IsAffectFlag(affect_flag)) { if (number(1, 1000) <= success_pct && !pkVictim->IsImmune(immune_flag)) diff --git a/src/game/char.cpp b/src/game/char.cpp index 769b292..50c5ef4 100644 --- a/src/game/char.cpp +++ b/src/game/char.cpp @@ -376,6 +376,11 @@ void CHARACTER::Initialize() memset(&m_tvLastSyncTime, 0, sizeof(m_tvLastSyncTime)); m_iSyncHackCount = 0; + + // MR-8: Snow dungeon - All-damage immunity with exceptions + m_bDamageImmune = false; + m_vecDamageImmunityConditions.clear(); + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions } void CHARACTER::Create(const char * c_pszName, DWORD vid, bool isPC) @@ -1502,6 +1507,17 @@ bool CHARACTER::Show(long lMapIndex, long x, long y, long z, bool bShowSpawnMoti REMOVE_BIT(m_bAddChrState, ADD_CHARACTER_STATE_SPAWN); SetValidComboInterval(0); + + // MR-8: Resync affects when moving within same map (fixes dungeon relog affect icon bug) + if (IsPC() && GetDesc() != NULL) { + // Re-send all affect packets to client to ensure icons are synchronized + itertype(m_list_pkAffect) it = m_list_pkAffect.begin(); + + while (it != m_list_pkAffect.end()) + SendAffectAddPacket(GetDesc(), *it++); + } + // MR-8: -- END OF -- Resync affects when moving within same map + return true; } @@ -7369,5 +7385,184 @@ int CHARACTER::GetSkillPowerByLevel(int level, bool bMob) const void CHARACTER::SetLastPMPulse(void) { - m_iLastPMPulse = thecore_pulse() + passes_per_sec; + m_iLastPMPulse = thecore_pulse() + passes_per_sec; } + + +// MR-8: Snow dungeon - All-damage immunity with exceptions +void CHARACTER::SetDamageImmunity(bool bImmune) +{ + m_bDamageImmune = bImmune; + + if (test_server) + { + DWORD vid = GetVID(); + + sys_log(0, "SetDamageImmunity: %s [%u] immune=%d conditions=%d", + GetName(), vid, bImmune, m_vecDamageImmunityConditions.size()); + } +} + +void CHARACTER::AddDamageImmunityCondition(BYTE bType, DWORD dwValue, const std::string& strExtra) +{ + SDamageImmunityCondition cond(bType, dwValue, strExtra); + m_vecDamageImmunityConditions.push_back(cond); + + if (test_server) + { + DWORD vid = GetVID(); + + sys_log(0, "AddDamageImmunityCondition: %s [%u] type=%d value=%u extra=%s", + GetName(), vid, bType, dwValue, strExtra.c_str()); + } +} + +void CHARACTER::ClearDamageImmunityConditions() +{ + m_vecDamageImmunityConditions.clear(); + + if (test_server) + { + DWORD vid = GetVID(); + + sys_log(0, "ClearDamageImmunityConditions: %s [%u]", GetName(), vid); + } +} + +bool CHARACTER::CheckDamageImmunityConditions(LPCHARACTER pAttacker) const +{ + if (!pAttacker) + return false; + + // If no conditions are set, block all damage + if (m_vecDamageImmunityConditions.empty()) + { + if (test_server && m_bDamageImmune) + { + DWORD vid = GetVID(); + + sys_err("CheckDamageImmunityConditions: %s [%u] has immunity flag but NO conditions - blocking damage", + GetName(), vid); + } + + return false; + } + + std::vector allowedJobs; + + // All conditions must pass (AND logic) + for (std::vector::const_iterator it = m_vecDamageImmunityConditions.begin(); + it != m_vecDamageImmunityConditions.end(); ++it) + { + const SDamageImmunityCondition& cond = *it; + + switch (cond.bType) + { + case DAMAGE_IMMUNITY_COND_AFFECT: + if (!pAttacker->FindAffect(cond.dwValue)) + { + if (test_server) + pAttacker->ChatPacket(CHAT_TYPE_INFO, "Target immune: need affect %u", cond.dwValue); + return false; + } + break; + + case DAMAGE_IMMUNITY_COND_LEVEL_MIN: + if (pAttacker->GetLevel() < (int)cond.dwValue) + { + if (test_server) + pAttacker->ChatPacket(CHAT_TYPE_INFO, "Target immune: need level >= %u", cond.dwValue); + return false; + } + break; + + case DAMAGE_IMMUNITY_COND_LEVEL_MAX: + if (pAttacker->GetLevel() > (int)cond.dwValue) + { + if (test_server) + pAttacker->ChatPacket(CHAT_TYPE_INFO, "Target immune: need level <= %u", cond.dwValue); + return false; + } + break; + + case DAMAGE_IMMUNITY_COND_QUEST_FLAG: + if (!pAttacker->IsPC()) + return false; + + if (pAttacker->GetQuestFlag(cond.strExtra) != (int)cond.dwValue) + { + if (test_server) + pAttacker->ChatPacket(CHAT_TYPE_INFO, "Target immune: need quest flag %s == %u", + cond.strExtra.c_str(), cond.dwValue); + return false; + } + break; + + case DAMAGE_IMMUNITY_COND_ITEM_EQUIPPED: + if (!pAttacker->IsEquipUniqueItem(cond.dwValue)) + { + if (test_server) + pAttacker->ChatPacket(CHAT_TYPE_INFO, "Target immune: need item %u equipped", cond.dwValue); + return false; + } + break; + + case DAMAGE_IMMUNITY_COND_EMPIRE: + if (pAttacker->GetEmpire() != (BYTE)cond.dwValue) + { + if (test_server) + pAttacker->ChatPacket(CHAT_TYPE_INFO, "Target immune: need empire %u", cond.dwValue); + return false; + } + break; + + case DAMAGE_IMMUNITY_COND_JOB: + if (!pAttacker->IsPC()) + return false; + + if ((BYTE)cond.dwValue >= JOB_MAX_NUM) + { + if (test_server) + pAttacker->ChatPacket(CHAT_TYPE_INFO, "Target immune: invalid job value"); + + return false; + } + + allowedJobs.push_back((BYTE)cond.dwValue); + break; + + default: + sys_err("Unknown damage immunity condition type: %d", cond.bType); + + return false; + } + } + + // Check job requirements (OR logic among jobs, AND with other conditions) + if (!allowedJobs.empty()) + { + bool bJobAllowed = false; + BYTE bJob = pAttacker->GetJob(); + + for (std::vector::const_iterator jt = allowedJobs.begin(); jt != allowedJobs.end(); ++jt) + { + if (bJob == *jt) + { + bJobAllowed = true; + break; + } + } + + if (!bJobAllowed) + { + if (test_server) + pAttacker->ChatPacket(CHAT_TYPE_INFO, "Target immune: need job match (your job: %d)", bJob); + + return false; + } + } + + // All conditions passed + return true; +} +// MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions diff --git a/src/game/char.h b/src/game/char.h index 2f7088b..47c6df4 100644 --- a/src/game/char.h +++ b/src/game/char.h @@ -1640,6 +1640,42 @@ class CHARACTER : public CEntity, public CFSM, public CHorseRider void ApplyMobAttribute(const TMobTable* table); // End of Resists & Proofs + //////////////////////////////////////////////////////////////////////////////////////// + // MR-8: Snow dungeon - All-damage immunity with exceptions + public: + enum EDamageImmunityConditionType + { + DAMAGE_IMMUNITY_COND_AFFECT = 0, // Has affect + DAMAGE_IMMUNITY_COND_LEVEL_MIN = 1, // Level >= value + DAMAGE_IMMUNITY_COND_LEVEL_MAX = 2, // Level <= value + DAMAGE_IMMUNITY_COND_QUEST_FLAG = 3, // Quest flag == value + DAMAGE_IMMUNITY_COND_ITEM_EQUIPPED = 4, // Has item vnum equipped + DAMAGE_IMMUNITY_COND_EMPIRE = 5, // Is specific empire + DAMAGE_IMMUNITY_COND_JOB = 6, // Is specific job + }; + + struct SDamageImmunityCondition + { + BYTE bType; + DWORD dwValue; + std::string strExtra; // For quest flag names, etc. + + SDamageImmunityCondition() : bType(0), dwValue(0) {} + SDamageImmunityCondition(BYTE t, DWORD v, const std::string& e = "") + : bType(t), dwValue(v), strExtra(e) {} + }; + + void SetDamageImmunity(bool bImmune); + bool IsDamageImmune() const { return m_bDamageImmune; } + void AddDamageImmunityCondition(BYTE bType, DWORD dwValue, const std::string& strExtra = ""); + void ClearDamageImmunityConditions(); + bool CheckDamageImmunityConditions(LPCHARACTER pAttacker) const; + + protected: + bool m_bDamageImmune; + std::vector m_vecDamageImmunityConditions; + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions + //////////////////////////////////////////////////////////////////////////////////////// // QUEST // diff --git a/src/game/char_battle.cpp b/src/game/char_battle.cpp index ff71254..e94c383 100644 --- a/src/game/char_battle.cpp +++ b/src/game/char_battle.cpp @@ -1769,6 +1769,39 @@ bool CHARACTER::Damage(LPCHARACTER pAttacker, int dam, EDamageType type) // retu //PROF_UNIT puAttr("Attr"); + // MR-8: Snow dungeon - All-damage immunity with exceptions + // Damage Immunity System - per-hit check (O(1) operation) + // Check immunity for monsters/stones/doors with the flag set + if (m_bDamageImmune && (IsMonster() || IsStone() || IsDoor())) + { + // Check if attacker meets all required conditions + // NOTE: If flag is set but conditions are empty, this will return false (block all damage) + if (!CheckDamageImmunityConditions(pAttacker)) + { + // Attacker doesn't meet conditions - send MISS packet directly + if (pAttacker->IsPC()) + { + TPacketGCDamageInfo damageInfo; + + memset(&damageInfo, 0, sizeof(TPacketGCDamageInfo)); + + damageInfo.header = HEADER_GC_DAMAGE_INFO; + damageInfo.dwVID = (DWORD)GetVID(); + damageInfo.flag = DAMAGE_DODGE; + damageInfo.damage = 0; + + if (pAttacker->GetDesc() != NULL) + { + pAttacker->GetDesc()->Packet(&damageInfo, sizeof(TPacketGCDamageInfo)); + } + } + + return false; + } + // All conditions met - allow damage to pass through + } + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions + // // ๋งˆ๋ฒ•ํ˜• ์Šคํ‚ฌ๊ณผ, ๋ ˆ์ธ์ง€ํ˜• ์Šคํ‚ฌ์€(๊ถ์ž๊ฐ) ํฌ๋ฆฌํ‹ฐ์ปฌ๊ณผ, ๊ด€ํ†ต๊ณต๊ฒฉ ๊ณ„์‚ฐ์„ ํ•œ๋‹ค. // ์›๋ž˜๋Š” ํ•˜์ง€ ์•Š์•„์•ผ ํ•˜๋Š”๋ฐ Nerf(๋‹ค์šด๋ฐธ๋Ÿฐ์Šค)ํŒจ์น˜๋ฅผ ํ•  ์ˆ˜ ์—†์–ด์„œ ํฌ๋ฆฌํ‹ฐ์ปฌ๊ณผ diff --git a/src/game/char_horse.cpp b/src/game/char_horse.cpp index 7dc7d99..76107e8 100644 --- a/src/game/char_horse.cpp +++ b/src/game/char_horse.cpp @@ -42,6 +42,15 @@ bool CHARACTER::StartRiding() return false; } + // MR-8: Prevent mounting in Nemere's Watchtower + long lMapIndex = GetMapIndex(); + + if (lMapIndex >= 352 * 10000 && lMapIndex < 353 * 10000) + { + ChatPacket(CHAT_TYPE_INFO, LC_TEXT("You cannot ride your horse in Nemere's Watchtower.")); + return false; + } + // MR-8: -- END OF -- Prevent mounting in Nemere's Watchtower DWORD dwMountVnum = m_chHorse ? m_chHorse->GetRaceNum() : GetMyHorseVnum(); @@ -151,6 +160,16 @@ void CHARACTER::HorseSummon(bool bSummon, bool bFromFar, DWORD dwVnum, const cha if (IsRiding()) return; + // MR-8: Prevent mounting in Nemere's Watchtower + long lMapIndex = GetMapIndex(); + + if (lMapIndex >= 352 * 10000 && lMapIndex < 353 * 10000) + { + ChatPacket(CHAT_TYPE_INFO, LC_TEXT("You cannot ride your horse in Nemere's Watchtower.")); + return; + } + // MR-8: -- END OF -- Prevent mounting in Nemere's Watchtower + sys_log(0, "HorseSummon : %s lv:%d bSummon:%d fromFar:%d", GetName(), GetLevel(), bSummon, bFromFar); long x = GetX(); @@ -357,10 +376,12 @@ bool CHARACTER::CanUseHorseSkill() { if(IsRiding()) { - if (GetHorseGrade() == 3) + // MR-8: CanUseHorseSkill() grade fix + if (GetHorseGrade() >= 3) return true; else return false; + // MR-8: -- END OF -- CanUseHorseSkill() grade fix if(GetMountVnum()) { diff --git a/src/game/char_item.cpp b/src/game/char_item.cpp index 09a0c2d..9ee57d0 100644 --- a/src/game/char_item.cpp +++ b/src/game/char_item.cpp @@ -7601,6 +7601,19 @@ bool CHARACTER::CanEquipNow(const LPITEM item, const TItemPos& srcCell, const TI return false; } + // MR-8: Prevent mounting in Nemere's Watchtower + if (item->GetSpecialGroup() == UNIQUE_GROUP_SPECIAL_RIDE) + { + long lMapIndex = GetMapIndex(); + bool isInNemereDungeon = lMapIndex >= 352 * 10000 && lMapIndex < 353 * 10000; + + if (isInNemereDungeon && !IsRiding()) + { + ChatPacket(CHAT_TYPE_INFO, LC_TEXT("You cannot ride your horse in Nemere's Watchtower.")); + return false; + } + } + // MR-8: -- END OF -- Prevent mounting in Nemere's Watchtower } return true; diff --git a/src/game/char_manager.cpp b/src/game/char_manager.cpp index 56fe881..35798ab 100644 --- a/src/game/char_manager.cpp +++ b/src/game/char_manager.cpp @@ -543,7 +543,8 @@ bool CHARACTER_MANAGER::SpawnMoveGroup(DWORD dwVnum, long lMapIndex, int sx, int return true; } -bool CHARACTER_MANAGER::SpawnGroupGroup(DWORD dwVnum, long lMapIndex, int sx, int sy, int ex, int ey, LPREGEN pkRegen, bool bAggressive_, LPDUNGEON pDungeon) +// MR-8: Snow dungeon - All-damage immunity with exceptions +LPCHARACTER CHARACTER_MANAGER::SpawnGroupWithVIDs(DWORD dwVnum, long lMapIndex, int sx, int sy, int ex, int ey, LPREGEN pkRegen, bool bAggressive_, LPDUNGEON pDungeon, std::vector& rVids) { const DWORD dwGroupID = CMobManager::Instance().GetGroupFromGroupGroup(dwVnum); @@ -601,6 +602,8 @@ LPCHARACTER CHARACTER_MANAGER::SpawnGroup(DWORD dwVnum, long lMapIndex, int sx, if (i == 0) chLeader = tch; + rVids.push_back(tch->GetVID()); + tch->SetDungeon(pDungeon); sx = tch->GetX() - number(300, 500); @@ -630,6 +633,93 @@ LPCHARACTER CHARACTER_MANAGER::SpawnGroup(DWORD dwVnum, long lMapIndex, int sx, return chLeader; } +LPCHARACTER CHARACTER_MANAGER::SpawnGroupWithImmunity(DWORD dwVnum, long lMapIndex, int sx, int sy, int ex, int ey, LPREGEN pkRegen, bool bAggressive_, LPDUNGEON pDungeon, const std::vector& conditions) +{ + CMobGroup * pkGroup = CMobManager::Instance().GetGroup(dwVnum); + + if (!pkGroup) + { + sys_err("NOT_EXIST_GROUP_VNUM(%u) Map(%u) ", dwVnum, lMapIndex); + return NULL; + } + + LPCHARACTER pkChrMaster = NULL; + LPPARTY pkParty = NULL; + + const std::vector & c_rdwMembers = pkGroup->GetMemberVector(); + + bool bSpawnedByStone = false; + bool bAggressive = bAggressive_; + + if (m_pkChrSelectedStone) + { + bSpawnedByStone = true; + + if (m_pkChrSelectedStone->GetDungeon()) + bAggressive = true; + } + + LPCHARACTER chLeader = NULL; + + for (DWORD i = 0; i < c_rdwMembers.size(); ++i) + { + LPCHARACTER tch = SpawnMobRange(c_rdwMembers[i], lMapIndex, sx, sy, ex, ey, true, bSpawnedByStone); + + if (!tch) + { + if (i == 0) // ๋ชป๋งŒ๋“  ๋ชฌ์Šคํ„ฐ๊ฐ€ ๋Œ€์žฅ์ผ ๊ฒฝ์šฐ์—๋Š” ๊ทธ๋ƒฅ ์‹คํŒจ + return NULL; + + continue; + } + + if (i == 0) + chLeader = tch; + + tch->SetDungeon(pDungeon); + + // Apply damage immunity atomically before mob is attackable + if (!conditions.empty()) + { + tch->SetDamageImmunity(true); + for (const auto& cond : conditions) + tch->AddDamageImmunityCondition(cond.bType, cond.dwValue, cond.strExtra); + } + + sx = tch->GetX() - number(300, 500); + sy = tch->GetY() - number(300, 500); + ex = tch->GetX() + number(300, 500); + ey = tch->GetY() + number(300, 500); + + if (m_pkChrSelectedStone) + tch->SetStone(m_pkChrSelectedStone); + else if (pkParty) + { + pkParty->Join(tch->GetVID()); + pkParty->Link(tch); + } + else if (!pkChrMaster) + { + pkChrMaster = tch; + pkChrMaster->SetRegen(pkRegen); + + pkParty = CPartyManager::instance().CreateParty(pkChrMaster); + } + + if (bAggressive) + tch->SetAggressive(); + } + + return chLeader; +} + +LPCHARACTER CHARACTER_MANAGER::SpawnGroup(DWORD dwVnum, long lMapIndex, int sx, int sy, int ex, int ey, LPREGEN pkRegen, bool bAggressive_, LPDUNGEON pDungeon) +{ + std::vector dummy; + return SpawnGroupWithVIDs(dwVnum, lMapIndex, sx, sy, ex, ey, pkRegen, bAggressive_, pDungeon, dummy); +} +// MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions + struct FuncUpdateAndResetChatCounter { void operator () (LPCHARACTER ch) diff --git a/src/game/char_manager.h b/src/game/char_manager.h index 31836ea..bb4d101 100644 --- a/src/game/char_manager.h +++ b/src/game/char_manager.h @@ -9,9 +9,10 @@ #include "common/length.h" #include "vid.h" +#include "char.h" class CDungeon; -class CHARACTER; +// class CHARACTER; class CharacterVectorInteractor; class CHARACTER_MANAGER : public singleton @@ -40,6 +41,10 @@ class CHARACTER_MANAGER : public singleton LPCHARACTER SpawnMob(DWORD dwVnum, long lMapIndex, long x, long y, long z, bool bSpawnMotion = false, int iRot = -1, bool bShow = true); LPCHARACTER SpawnMobRange(DWORD dwVnum, long lMapIndex, int sx, int sy, int ex, int ey, bool bIsException=false, bool bSpawnMotion = false , bool bAggressive = false); LPCHARACTER SpawnGroup(DWORD dwVnum, long lMapIndex, int sx, int sy, int ex, int ey, LPREGEN pkRegen = NULL, bool bAggressive_ = false, LPDUNGEON pDungeon = NULL); + // MR-8: Snow dungeon - All-damage immunity with exceptions + LPCHARACTER SpawnGroupWithVIDs(DWORD dwVnum, long lMapIndex, int sx, int sy, int ex, int ey, LPREGEN pkRegen, bool bAggressive_, LPDUNGEON pDungeon, std::vector& rVids); + LPCHARACTER SpawnGroupWithImmunity(DWORD dwVnum, long lMapIndex, int sx, int sy, int ex, int ey, LPREGEN pkRegen, bool bAggressive_, LPDUNGEON pDungeon, const std::vector& conditions); + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions bool SpawnGroupGroup(DWORD dwVnum, long lMapIndex, int sx, int sy, int ex, int ey, LPREGEN pkRegen = NULL, bool bAggressive_ = false, LPDUNGEON pDungeon = NULL); bool SpawnMoveGroup(DWORD dwVnum, long lMapIndex, int sx, int sy, int ex, int ey, int tx, int ty, LPREGEN pkRegen = NULL, bool bAggressive_ = false); LPCHARACTER SpawnMobRandomPosition(DWORD dwVnum, long lMapIndex); diff --git a/src/game/char_resist.cpp b/src/game/char_resist.cpp index 795aa45..f8ba26e 100644 --- a/src/game/char_resist.cpp +++ b/src/game/char_resist.cpp @@ -167,7 +167,17 @@ void CHARACTER::AttackedByFire(LPCHARACTER pkAttacker, int amount, int count) if (m_pkFireEvent) return; - AddAffect(AFFECT_FIRE, POINT_NONE, 0, AFF_FIRE, count*3+1, 0, true); + // MR-8: Check damage immunity system - prevent fire application if conditions not met + if (m_bDamageImmune && (IsMonster() || IsStone() || IsDoor())) + { + if (!CheckDamageImmunityConditions(pkAttacker)) + { + return; // Immunity prevents fire application + } + } + + AddAffect(AFFECT_FIRE, POINT_NONE, 0, AFF_FIRE, count * 3 + 1, 0, true); + // MR-8: -- END OF -- Check damage immunity system - prevent fire application if conditions not met TFireEventInfo* info = AllocEventInfo(); @@ -187,6 +197,16 @@ void CHARACTER::AttackedByPoison(LPCHARACTER pkAttacker) if (m_bHasPoisoned && !IsPC()) // ๋ชฌ์Šคํ„ฐ๋Š” ๋…์ด ํ•œ๋ฒˆ๋งŒ ๊ฑธ๋ฆฐ๋‹ค. return; + // MR-8: Check damage immunity system - prevent poison application if conditions not met + if (m_bDamageImmune && (IsMonster() || IsStone() || IsDoor())) + { + if (!CheckDamageImmunityConditions(pkAttacker)) + { + return; // Immunity prevents poison application + } + } + // MR-8: -- END OF -- Check damage immunity system - prevent poison application if conditions not met + if (pkAttacker && pkAttacker->GetLevel() < GetLevel()) { int delta = GetLevel() - pkAttacker->GetLevel(); diff --git a/src/game/char_skill.cpp b/src/game/char_skill.cpp index 67ed1cb..1700829 100644 --- a/src/game/char_skill.cpp +++ b/src/game/char_skill.cpp @@ -1441,13 +1441,14 @@ struct FuncSplashDamage iDur += m_pkChr->GetPoint(POINT_PARTY_BUFFER_BONUS); + // MR-8: Snow dungeon - All-damage immunity with exceptions if (IS_SET(m_pkSk->dwFlag, SKILL_FLAG_STUN)) { - SkillAttackAffect(pkChrVictim, iPct, IMMUNE_STUN, AFFECT_STUN, POINT_NONE, 0, AFF_STUN, iDur, m_pkSk->szName); + SkillAttackAffect(m_pkChr, pkChrVictim, iPct, IMMUNE_STUN, AFFECT_STUN, POINT_NONE, 0, AFF_STUN, iDur, m_pkSk->szName); } else if (IS_SET(m_pkSk->dwFlag, SKILL_FLAG_SLOW)) { - SkillAttackAffect(pkChrVictim, iPct, IMMUNE_SLOW, AFFECT_SLOW, POINT_MOV_SPEED, -30, AFF_SLOW, iDur, m_pkSk->szName); + SkillAttackAffect(m_pkChr, pkChrVictim, iPct, IMMUNE_SLOW, AFFECT_SLOW, POINT_MOV_SPEED, -30, AFF_SLOW, iDur, m_pkSk->szName); } else if (IS_SET(m_pkSk->dwFlag, SKILL_FLAG_FIRE_CONT)) { @@ -1472,6 +1473,7 @@ struct FuncSplashDamage if (number(1, 100) <= iPct) pkChrVictim->AttackedByPoison(m_pkChr); } + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions } if (IS_SET(m_pkSk->dwFlag, SKILL_FLAG_CRUSH | SKILL_FLAG_CRUSH_LONG) && @@ -1510,11 +1512,13 @@ struct FuncSplashDamage if (m_pkChr->IsPC() && m_pkChr->m_SkillUseInfo[m_pkSk->dwVnum].GetMainTargetVID() == (DWORD) pkChrVictim->GetVID()) { + // MR-8: Snow dungeon - All-damage immunity with exceptions //if (!g_iUseLocale) if (LC_IsYMIR()) - SkillAttackAffect(pkChrVictim, 1000, IMMUNE_STUN, m_pkSk->dwVnum, POINT_NONE, 0, AFF_STUN, 3, m_pkSk->szName); + SkillAttackAffect(m_pkChr, pkChrVictim, 1000, IMMUNE_STUN, m_pkSk->dwVnum, POINT_NONE, 0, AFF_STUN, 3, m_pkSk->szName); else - SkillAttackAffect(pkChrVictim, 1000, IMMUNE_STUN, m_pkSk->dwVnum, POINT_NONE, 0, AFF_STUN, 4, m_pkSk->szName); + SkillAttackAffect(m_pkChr, pkChrVictim, 1000, IMMUNE_STUN, m_pkSk->dwVnum, POINT_NONE, 0, AFF_STUN, 4, m_pkSk->szName); + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions } else { @@ -1985,9 +1989,14 @@ int CHARACTER::ComputeSkill(DWORD dwVnum, LPCHARACTER pkVictim, BYTE bSkillLevel CSkillProto* pkSk = CSkillManager::instance().Get(dwVnum); const bool bCanUseHorseSkill = CanUseHorseSkill(); - // ๋ง์„ ํƒ€๊ณ ์žˆ์ง€๋งŒ ์Šคํ‚ฌ์€ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋Š” ์ƒํƒœ๋ผ๋ฉด return - if (false == bCanUseHorseSkill && true == IsRiding()) - return BATTLE_NONE; + // MR-8: Flame Ghost skill fix on mounting + if (dwVnum != SKILL_MUYEONG) + { + // ๋ง์„ ํƒ€๊ณ ์žˆ์ง€๋งŒ ์Šคํ‚ฌ์€ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋Š” ์ƒํƒœ๋ผ๋ฉด return + if (false == bCanUseHorseSkill && true == IsRiding()) + return BATTLE_NONE; + } + // MR-8: -- END OF -- Flame Ghost skill fix on mounting if (IsPolymorphed()) return BATTLE_NONE; @@ -1998,12 +2007,16 @@ int CHARACTER::ComputeSkill(DWORD dwVnum, LPCHARACTER pkVictim, BYTE bSkillLevel if (!pkSk) return BATTLE_NONE; - if (bCanUseHorseSkill && pkSk->dwType != SKILL_TYPE_HORSE) - return BATTLE_NONE; + // MR-8: Flame Ghost skill fix on mounting + if (dwVnum != SKILL_MUYEONG) + { + if (bCanUseHorseSkill && pkSk->dwType != SKILL_TYPE_HORSE) + return BATTLE_NONE; - if (!bCanUseHorseSkill && pkSk->dwType == SKILL_TYPE_HORSE) - return BATTLE_NONE; - + if (!bCanUseHorseSkill && pkSk->dwType == SKILL_TYPE_HORSE) + return BATTLE_NONE; + } + // MR-8: -- END OF -- Flame Ghost skill fix on mounting // ์ƒ๋Œ€๋ฐฉ์—๊ฒŒ ์“ฐ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ฉด ๋‚˜์—๊ฒŒ ์จ์•ผ ํ•œ๋‹ค. if (IS_SET(pkSk->dwFlag, SKILL_FLAG_SELFONLY)) diff --git a/src/game/cmd_gm.cpp b/src/game/cmd_gm.cpp index ba8959c..03862f5 100644 --- a/src/game/cmd_gm.cpp +++ b/src/game/cmd_gm.cpp @@ -65,15 +65,17 @@ void Command_ApplyAffect(LPCHARACTER ch, const char* argument, const char* affec return; } + // MR-8: Snow dungeon - All-damage immunity with exceptions switch (cmdAffect) { case COMMANDAFFECT_STUN: - SkillAttackAffect(tch, 1000, IMMUNE_STUN, AFFECT_STUN, POINT_NONE, 0, AFF_STUN, 30, "GM_STUN"); + SkillAttackAffect(ch, tch, 1000, IMMUNE_STUN, AFFECT_STUN, POINT_NONE, 0, AFF_STUN, 30, "GM_STUN"); break; case COMMANDAFFECT_SLOW: - SkillAttackAffect(tch, 1000, IMMUNE_SLOW, AFFECT_SLOW, POINT_MOV_SPEED, -30, AFF_SLOW, 30, "GM_SLOW"); + SkillAttackAffect(ch, tch, 1000, IMMUNE_SLOW, AFFECT_SLOW, POINT_MOV_SPEED, -30, AFF_SLOW, 30, "GM_SLOW"); break; } + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions sys_log(0, "%s %s", arg1, affectName); diff --git a/src/game/dungeon.cpp b/src/game/dungeon.cpp index 9536e84..8da3642 100644 --- a/src/game/dungeon.cpp +++ b/src/game/dungeon.cpp @@ -763,6 +763,46 @@ LPCHARACTER CDungeon::SpawnMob_ac_dir(DWORD vnum, int x, int y, int dir) return ch; } +// MR-8: Snow dungeon - All-damage immunity with exceptions +LPCHARACTER CDungeon::SpawnMobWithImmunity(DWORD vnum, int x, int y, int dir, const std::vector& conditions) +{ + LPSECTREE_MAP pkSectreeMap = SECTREE_MANAGER::instance().GetMap(m_lMapIndex); + if (pkSectreeMap == NULL) { + sys_err("CDungeon: SECTREE_MAP not found for #%ld", m_lMapIndex); + return NULL; + } + + long ax = pkSectreeMap->m_setting.iBaseX + x * 100; + long ay = pkSectreeMap->m_setting.iBaseY + y * 100; + + int rot = (dir == 0 ? -1 : (dir - 1) * 45); + + // Create but do NOT show yet + LPCHARACTER ch = CHARACTER_MANAGER::instance().SpawnMob(vnum, m_lMapIndex, ax, ay, 0, false, rot, false); + if (!ch) + return NULL; + + // Apply damage immunity atomically before showing + if (!conditions.empty()) + { + ch->SetDamageImmunity(true); + for (const auto& c : conditions) + ch->AddDamageImmunityCondition(c.bType, c.dwValue, c.strExtra); + } + + // Link to dungeon and show + ch->SetDungeon(this); + if (!ch->Show(m_lMapIndex, ax, ay, 0, false)) + { + M2_DESTROY_CHARACTER(ch); + sys_log(0, "SpawnMobWithImmunity: cannot show monster"); + return NULL; + } + + return ch; +} +// MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions + void CDungeon::SpawnNameMob(DWORD vnum, int x, int y, const char* name) { LPSECTREE_MAP pkSectreeMap = SECTREE_MANAGER::instance().GetMap(m_lMapIndex); @@ -833,6 +873,67 @@ LPCHARACTER CDungeon::SpawnGroup(DWORD vnum, long x, long y, float radius, bool return ch; } +// MR-8: Snow dungeon - All-damage immunity with exceptions +bool CDungeon::SpawnGroupWithVIDs(DWORD vnum, long x, long y, float radius, bool bAggressive, int count, std::vector& rVids) +{ + LPSECTREE_MAP pkSectreeMap = SECTREE_MANAGER::instance().GetMap(m_lMapIndex); + if (pkSectreeMap == NULL) { + sys_err("CDungeon: SECTREE_MAP not found for #%ld", m_lMapIndex); + return false; + } + + int iRadius = (int) radius; + + int sx = pkSectreeMap->m_setting.iBaseX + x - iRadius; + int sy = pkSectreeMap->m_setting.iBaseY + y - iRadius; + int ex = sx + iRadius; + int ey = sy + iRadius; + + bool bAny = false; + + while (count--) + { + std::vector localVids; + LPCHARACTER chLeader = CHARACTER_MANAGER::instance().SpawnGroupWithVIDs(vnum, m_lMapIndex, sx, sy, ex, ey, NULL, bAggressive, this, localVids); + if (chLeader) + { + bAny = true; + if (!localVids.empty()) + rVids.insert(rVids.end(), localVids.begin(), localVids.end()); + } + } + + return bAny; +} + +LPCHARACTER CDungeon::SpawnGroupWithImmunity(DWORD vnum, long x, long y, float radius, bool bAggressive, int count, const std::vector& conditions) +{ + LPSECTREE_MAP pkSectreeMap = SECTREE_MANAGER::instance().GetMap(m_lMapIndex); + if (pkSectreeMap == NULL) { + sys_err("CDungeon: SECTREE_MAP not found for #%ld", m_lMapIndex); + return NULL; + } + + int iRadius = (int) radius; + + int sx = pkSectreeMap->m_setting.iBaseX + x - iRadius; + int sy = pkSectreeMap->m_setting.iBaseY + y - iRadius; + int ex = sx + iRadius; + int ey = sy + iRadius; + + LPCHARACTER chLeader = NULL; + + while (count--) + { + LPCHARACTER ch = CHARACTER_MANAGER::instance().SpawnGroupWithImmunity(vnum, m_lMapIndex, sx, sy, ex, ey, NULL, bAggressive, this, conditions); + if (ch && !chLeader) + chLeader = ch; + } + + return chLeader; +} +// MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions + void CDungeon::SpawnRegen(const char* filename, bool bOnce) { if (!filename) @@ -850,6 +951,44 @@ void CDungeon::SpawnRegen(const char* filename, bool bOnce) regen_do(filename, m_lMapIndex, pkSectreeMap->m_setting.iBaseX, pkSectreeMap->m_setting.iBaseY, this, bOnce); } +// MR-8: Snow dungeon - All-damage immunity with exceptions +bool CDungeon::SpawnRegenWithVIDs(const char* filename, bool bOnce, std::vector& rVids) +{ + if (!filename) + { + sys_err("CDungeon::SpawnRegenWithVIDs(filename=NULL, bOnce=%d) - m_lMapIndex[%d]", bOnce, m_lMapIndex); + return false; + } + + LPSECTREE_MAP pkSectreeMap = SECTREE_MANAGER::instance().GetMap(m_lMapIndex); + if (!pkSectreeMap) + { + sys_err("CDungeon::SpawnRegenWithVIDs(filename=%s, bOnce=%d) - m_lMapIndex[%d]", filename, bOnce, m_lMapIndex); + return false; + } + + return regen_do(filename, m_lMapIndex, pkSectreeMap->m_setting.iBaseX, pkSectreeMap->m_setting.iBaseY, this, bOnce, &rVids); +} + +void CDungeon::SpawnRegenWithImmunity(const char* filename, bool bOnce, const std::vector& conditions) +{ + if (!filename) + { + sys_err("CDungeon::SpawnRegenWithImmunity(filename=NULL, bOnce=%d) - m_lMapIndex[%d]", bOnce, m_lMapIndex); + return; + } + + LPSECTREE_MAP pkSectreeMap = SECTREE_MANAGER::instance().GetMap(m_lMapIndex); + if (!pkSectreeMap) + { + sys_err("CDungeon::SpawnRegenWithImmunity(filename=%s, bOnce=%d) - m_lMapIndex[%d]", filename, bOnce, m_lMapIndex); + return; + } + + regen_do(filename, m_lMapIndex, pkSectreeMap->m_setting.iBaseX, pkSectreeMap->m_setting.iBaseY, this, bOnce, NULL, &conditions); +} +// MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions + void CDungeon::AddRegen(LPREGEN regen) { regen->id = regen_id_++; diff --git a/src/game/dungeon.h b/src/game/dungeon.h index bbeb983..a9a6597 100644 --- a/src/game/dungeon.h +++ b/src/game/dungeon.h @@ -1,7 +1,9 @@ ๏ปฟ#ifndef __INC_METIN_II_GAME_DUNGEON_H #define __INC_METIN_II_GAME_DUNGEON_H +#include #include "sectree_manager.h" +#include "char.h" class CParty; @@ -57,12 +59,21 @@ class CDungeon void Spawn(DWORD vnum, const char* pos); LPCHARACTER SpawnMob(DWORD vnum, int x, int y, int dir = 0); LPCHARACTER SpawnMob_ac_dir(DWORD vnum, int x, int y, int dir = 0); - LPCHARACTER SpawnGroup(DWORD vnum, long x, long y, float radius, bool bAggressive=false, int count=1); + // MR-8: Snow dungeon - All-damage immunity with exceptions + LPCHARACTER SpawnMobWithImmunity(DWORD vnum, int x, int y, int dir, const std::vector& conditions); + LPCHARACTER SpawnGroup(DWORD vnum, long x, long y, float radius, bool bAggressive = false, int count = 1); + bool SpawnGroupWithVIDs(DWORD vnum, long x, long y, float radius, bool bAggressive, int count, std::vector& rVids); + LPCHARACTER SpawnGroupWithImmunity(DWORD vnum, long x, long y, float radius, bool bAggressive, int count, const std::vector& conditions); + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions void SpawnNameMob(DWORD vnum, int x, int y, const char* name); void SpawnGotoMob(long lFromX, long lFromY, long lToX, long lToY); void SpawnRegen(const char* filename, bool bOnce = true); + // MR-8: Snow dungeon - All-damage immunity with exceptions + bool SpawnRegenWithVIDs(const char* filename, bool bOnce, std::vector& rVids); + void SpawnRegenWithImmunity(const char* filename, bool bOnce, const std::vector& conditions); + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions void AddRegen(LPREGEN regen); void ClearRegen(); bool IsValidRegen(LPREGEN regen, size_t regen_id); diff --git a/src/game/input_db.cpp b/src/game/input_db.cpp index 051ad2e..2b9f33f 100644 --- a/src/game/input_db.cpp +++ b/src/game/input_db.cpp @@ -1047,6 +1047,13 @@ EVENTFUNC(quest_login_event) } else if (d->IsPhase(PHASE_GAME)) { + // MR-8: Fix "when_login" being loaded before character affects + if (!ch->IsLoadedAffect()) + { + return PASSES_PER_SEC(1); + } + // MR-8: -- END OF -- Fix "when_login" being loaded before character affects + sys_log(0, "QUEST_LOAD: Login pc %d by event", ch->GetPlayerID()); quest::CQuestManager::instance().Login(ch->GetPlayerID()); return 0; @@ -1110,18 +1117,12 @@ void CInputDB::QuestLoad(LPDESC d, const char * c_pData) pkPC->SetLoaded(); pkPC->Build(); - if (ch->GetDesc()->IsPhase(PHASE_GAME)) - { - sys_log(0, "QUEST_LOAD: Login pc %d", pQuestTable[0].dwPID); - quest::CQuestManager::instance().Login(pQuestTable[0].dwPID); - } - else - { - quest_login_event_info* info = AllocEventInfo(); - info->dwPID = ch->GetPlayerID(); + // MR-8: Fix "when_login" being loaded before character affects + quest_login_event_info* info = AllocEventInfo(); + info->dwPID = ch->GetPlayerID(); - event_create(quest_login_event, info, PASSES_PER_SEC(1)); - } + event_create(quest_login_event, info, PASSES_PER_SEC(1)); + // MR-8: -- END OF -- Fix "when_login" being loaded before character affects } } diff --git a/src/game/input_login.cpp b/src/game/input_login.cpp index fc2b3d3..fa502e8 100644 --- a/src/game/input_login.cpp +++ b/src/game/input_login.cpp @@ -700,17 +700,38 @@ void CInputLogin::Entergame(LPDESC d, const char * data) else if (marriage::WeddingManager::instance().IsWeddingMap(ch->GetMapIndex())) ch->SetWeddingMap(marriage::WeddingManager::instance().Find(ch->GetMapIndex())); else { + // MR-8: Auto-unmount and unequip special ride seals in Nemere's Watchtower + if (ch->GetMapIndex() >= 352 * 10000 && ch->GetMapIndex() < 353 * 10000) + { + ch->RemoveAffect(AFFECT_MOUNT); + ch->RemoveAffect(AFFECT_MOUNT_BONUS); + + if (ch->IsRiding() || ch->IsHorseRiding()) + { + ch->StopRiding(); + ch->HorseSummon(false); + } + + // Check for inventory space + int emptyCell = ch->GetEmptyInventory(1); + + if (emptyCell != -1) + ch->UnEquipSpecialRideUniqueItem(); + } + // MR-8: -- END OF -- Auto-unmount and unequip special ride seals in Nemere's Watchtower + ch->SetDungeon(CDungeonManager::instance().FindByMapIndex(ch->GetMapIndex())); } } else if (CArenaManager::instance().IsArenaMap(ch->GetMapIndex()) == true) { int memberFlag = CArenaManager::instance().IsMember(ch->GetMapIndex(), ch->GetPlayerID()); + if (memberFlag == MEMBER_OBSERVER) { ch->SetObserverMode(true); ch->SetArenaObserverMode(true); - if (CArenaManager::instance().RegisterObserverPtr(ch, ch->GetMapIndex(), ch->GetX()/100, ch->GetY()/100)) + if (CArenaManager::instance().RegisterObserverPtr(ch, ch->GetMapIndex(), ch->GetX() / 100, ch->GetY() / 100)) { sys_log(0, "ARENA : Observer add failed"); } diff --git a/src/game/item_manager_read_tables.cpp b/src/game/item_manager_read_tables.cpp index 3e60697..5e20f7c 100644 --- a/src/game/item_manager_read_tables.cpp +++ b/src/game/item_manager_read_tables.cpp @@ -221,44 +221,47 @@ bool ITEM_MANAGER::ReadSpecialDropItemFile(const char * c_pszFileName) const std::string& name = pTok->at(0); DWORD dwVnum = 0; - if (!GetVnumByOriginalName(name.c_str(), dwVnum)) + // MR-8: Special_Item_Group Fix for handling gold and experience + if (name == "elk") { - if (name == "exp") + dwVnum = CSpecialItemGroup::GOLD; + } + else if (name == "exp") + { + dwVnum = CSpecialItemGroup::EXP; + } + else if (name == "mob") + { + dwVnum = CSpecialItemGroup::MOB; + } + else if (name == "slow") + { + dwVnum = CSpecialItemGroup::SLOW; + } + else if (name == "drain_hp") + { + dwVnum = CSpecialItemGroup::DRAIN_HP; + } + else if (name == "poison") + { + dwVnum = CSpecialItemGroup::POISON; + } + else if (name == "group") + { + dwVnum = CSpecialItemGroup::MOB_GROUP; + } + else if (!GetVnumByOriginalName(name.c_str(), dwVnum)) + { + str_to_number(dwVnum, name.c_str()); + if (!ITEM_MANAGER::instance().GetTable(dwVnum)) { - dwVnum = CSpecialItemGroup::EXP; - } - else if (name == "mob") - { - dwVnum = CSpecialItemGroup::MOB; - } - else if (name == "slow") - { - dwVnum = CSpecialItemGroup::SLOW; - } - else if (name == "drain_hp") - { - dwVnum = CSpecialItemGroup::DRAIN_HP; - } - else if (name == "poison") - { - dwVnum = CSpecialItemGroup::POISON; - } - else if (name == "group") - { - dwVnum = CSpecialItemGroup::MOB_GROUP; - } - else - { - str_to_number(dwVnum, name.c_str()); - if (!ITEM_MANAGER::instance().GetTable(dwVnum)) - { - sys_err("ReadSpecialDropItemFile : there is no item %s : node %s", name.c_str(), stName.c_str()); - M2_DELETE(pkGroup); + sys_err("ReadSpecialDropItemFile : there is no item %s : node %s", name.c_str(), stName.c_str()); + M2_DELETE(pkGroup); - return false; - } + return false; } } + // MR-8: -- END OF -- Special_Item_Group Fix for handling gold and experience int iCount = 0; str_to_number(iCount, pTok->at(1).c_str()); @@ -286,7 +289,9 @@ bool ITEM_MANAGER::ReadSpecialDropItemFile(const char * c_pszFileName) break; } + loader.SetParentNode(); + if (CSpecialItemGroup::QUEST == type) { m_map_pkQuestItemGroup.insert(std::make_pair(iVnum, pkGroup)); @@ -309,6 +314,7 @@ bool ITEM_MANAGER::ConvSpecialDropItemFile() "%s/special_item_group.txt", LocaleService_GetBasePath().c_str()); FILE *fp = fopen("special_item_group_vnum.txt", "w"); + if (!fp) { sys_err("could not open file (%s)", "special_item_group_vnum.txt"); @@ -372,13 +378,16 @@ bool ITEM_MANAGER::ConvSpecialDropItemFile() if (!GetVnumByOriginalName(name.c_str(), dwVnum)) { + // MR-8: Special_Item_Group Fix for handling gold and experience //if ( name == "๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝฤก" || - if (name == "exp" || + if (name == "elk" || + name == "exp" || name == "mob" || name == "slow" || name == "drain_hp" || name == "poison" || name == "group") + // MR-8: -- END OF -- Special_Item_Group Fix for handling gold and experience { dwVnum = 0; } diff --git a/src/game/questlua_dungeon.cpp b/src/game/questlua_dungeon.cpp index 2b7d6f2..a4a998f 100644 --- a/src/game/questlua_dungeon.cpp +++ b/src/game/questlua_dungeon.cpp @@ -11,6 +11,8 @@ #include "desc_client.h" #include "desc_manager.h" +#include + #undef sys_err #define sys_err(fmt, ...) quest::CQuestManager::instance().QuestError(std::source_location::current(), fmt __VA_OPT__(, __VA_ARGS__)) @@ -173,6 +175,40 @@ namespace quest return 0; } + // MR-8: Snow dungeon - All-damage immunity with exceptions + int dungeon_regen_file_with_vids(lua_State* L) + { + if (!lua_isstring(L,1)) + { + sys_err("wrong filename"); + lua_newtable(L); + return 1; + } + + CQuestManager& q = CQuestManager::instance(); + LPDUNGEON pDungeon = q.GetCurrentDungeon(); + + std::vector vids; + + if (pDungeon) + { + const char* filename = lua_tostring(L, 1); + pDungeon->SpawnRegenWithVIDs(filename, true, vids); + } + + lua_newtable(L); + + for (size_t i = 0; i < vids.size(); ++i) + { + lua_pushnumber(L, i + 1); + lua_pushnumber(L, vids[i]); + lua_settable(L, -3); + } + + return 1; + } + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions + int dungeon_set_regen_file(lua_State* L) { if (!lua_isstring(L,1)) @@ -971,6 +1007,207 @@ namespace quest return 1; } + // MR-8: Snow dungeon - All-damage immunity with exceptions + int dungeon_spawn_group_with_vids(lua_State* L) + { + // + // argument: vnum,x,y,radius,aggressive,count + // returns: table of all spawned VIDs + // + if (!lua_isnumber(L, 1) || !lua_isnumber(L, 2) || !lua_isnumber(L, 3) || !lua_isnumber(L, 4) || !lua_isnumber(L, 6)) + { + sys_err("invalid argument"); + lua_newtable(L); + + return 1; + } + + CQuestManager& q = CQuestManager::instance(); + LPDUNGEON pDungeon = q.GetCurrentDungeon(); + + std::vector vids; + + if (pDungeon) + { + DWORD group_vnum = (DWORD)lua_tonumber(L, 1); + long local_x = (long) lua_tonumber(L, 2) * 100; + long local_y = (long) lua_tonumber(L, 3) * 100; + float radius = (float) lua_tonumber(L, 4) * 100; + bool bAggressive = lua_toboolean(L, 5); + DWORD count = (DWORD) lua_tonumber(L, 6); + + pDungeon->SpawnGroupWithVIDs(group_vnum, local_x, local_y, radius, bAggressive, count, vids); + } + + // Push table of VIDs to Lua + lua_newtable(L); + + for (size_t i = 0; i < vids.size(); ++i) + { + lua_pushnumber(L, i + 1); + lua_pushnumber(L, vids[i]); + lua_settable(L, -3); + } + + return 1; + } + + int dungeon_spawn_group_with_immunity(lua_State* L) + { + // + // argument: vnum, x, y, radius, aggressive, count, conditions_table + // conditions_table: { {type=6, value=3}, ... } + // returns: vid of group leader + // + if (!lua_isnumber(L, 1) || !lua_isnumber(L, 2) || !lua_isnumber(L, 3) || + !lua_isnumber(L, 4) || !lua_isnumber(L, 6) || !lua_istable(L, 7)) + { + sys_err("invalid argument"); + lua_pushnumber(L, 0); + return 1; + } + + CQuestManager& q = CQuestManager::instance(); + LPDUNGEON pDungeon = q.GetCurrentDungeon(); + + if (!pDungeon) + { + lua_pushnumber(L, 0); + return 1; + } + + DWORD group_vnum = (DWORD)lua_tonumber(L, 1); + long local_x = (long) lua_tonumber(L, 2) * 100; + long local_y = (long) lua_tonumber(L, 3) * 100; + float radius = (float) lua_tonumber(L, 4) * 100; + bool bAggressive = lua_toboolean(L, 5); + DWORD count = (DWORD) lua_tonumber(L, 6); + + // Parse conditions table + std::vector conditions; + lua_pushnil(L); + while (lua_next(L, 7) != 0) + { + if (lua_istable(L, -1)) + { + lua_pushstring(L, "type"); + lua_gettable(L, -2); + BYTE bType = (BYTE)lua_tonumber(L, -1); + lua_pop(L, 1); + + lua_pushstring(L, "value"); + lua_gettable(L, -2); + DWORD dwValue = (DWORD)lua_tonumber(L, -1); + lua_pop(L, 1); + + conditions.emplace_back(bType, dwValue); + } + lua_pop(L, 1); + } + + LPCHARACTER ch = pDungeon->SpawnGroupWithImmunity(group_vnum, local_x, local_y, radius, bAggressive, count, conditions); + lua_pushnumber(L, ch ? ch->GetVID() : 0); + return 1; + } + + int dungeon_spawn_mob_with_immunity(lua_State* L) + { + // argument: vnum, x, y, conditions_table + // returns: vid + if (!lua_isnumber(L, 1) || !lua_isnumber(L, 2) || !lua_isnumber(L, 3) || !lua_istable(L, 4)) + { + sys_err("invalid argument"); + lua_pushnumber(L, 0); + return 1; + } + + CQuestManager& q = CQuestManager::instance(); + LPDUNGEON pDungeon = q.GetCurrentDungeon(); + if (!pDungeon) + { + lua_pushnumber(L, 0); + return 1; + } + + DWORD mob_vnum = (DWORD)lua_tonumber(L, 1); + int x = (int)lua_tonumber(L, 2); + int y = (int)lua_tonumber(L, 3); + + // Parse conditions + std::vector conditions; + lua_pushnil(L); + while (lua_next(L, 4) != 0) + { + if (lua_istable(L, -1)) + { + lua_pushstring(L, "type"); + lua_gettable(L, -2); + BYTE bType = (BYTE)lua_tonumber(L, -1); + lua_pop(L, 1); + + lua_pushstring(L, "value"); + lua_gettable(L, -2); + DWORD dwValue = (DWORD)lua_tonumber(L, -1); + lua_pop(L, 1); + + conditions.emplace_back(bType, dwValue); + } + lua_pop(L, 1); + } + + LPCHARACTER ch = pDungeon->SpawnMobWithImmunity(mob_vnum, x, y, 0, conditions); + lua_pushnumber(L, ch ? ch->GetVID() : 0); + return 1; + } + + int dungeon_regen_file_with_immunity(lua_State* L) + { + // + // argument: filename, conditions_table + // conditions_table: { {type=6, value=3}, ... } + // returns: nothing + // + if (!lua_isstring(L, 1) || !lua_istable(L, 2)) + { + sys_err("invalid argument"); + return 0; + } + + CQuestManager& q = CQuestManager::instance(); + LPDUNGEON pDungeon = q.GetCurrentDungeon(); + + if (!pDungeon) + return 0; + + const char* filename = lua_tostring(L, 1); + + // Parse conditions table + std::vector conditions; + lua_pushnil(L); + while (lua_next(L, 2) != 0) + { + if (lua_istable(L, -1)) + { + lua_pushstring(L, "type"); + lua_gettable(L, -2); + BYTE bType = (BYTE)lua_tonumber(L, -1); + lua_pop(L, 1); + + lua_pushstring(L, "value"); + lua_gettable(L, -2); + DWORD dwValue = (DWORD)lua_tonumber(L, -1); + lua_pop(L, 1); + + conditions.emplace_back(bType, dwValue); + } + lua_pop(L, 1); + } + + pDungeon->SpawnRegenWithImmunity(filename, true, conditions); + return 0; + } + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions + int dungeon_join(lua_State* L) { if (lua_gettop(L) < 1 || !lua_isnumber(L, 1)) @@ -1438,6 +1675,11 @@ namespace quest { "spawn_name_mob", dungeon_spawn_name_mob }, { "spawn_goto_mob", dungeon_spawn_goto_mob }, { "spawn_group", dungeon_spawn_group }, + // MR-8: Snow dungeon - All-damage immunity with exceptions + { "spawn_group_with_vids", dungeon_spawn_group_with_vids }, + { "spawn_group_with_immunity", dungeon_spawn_group_with_immunity }, + { "spawn_mob_with_immunity", dungeon_spawn_mob_with_immunity }, + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions { "spawn_unique", dungeon_spawn_unique }, { "spawn_move_unique", dungeon_spawn_move_unique}, { "spawn_move_group", dungeon_spawn_move_group}, @@ -1463,6 +1705,10 @@ namespace quest { "new_jump_party", dungeon_new_jump_party }, { "new_jump", dungeon_new_jump }, { "regen_file", dungeon_regen_file }, + // MR-8: Snow dungeon - All-damage immunity with exceptions + { "regen_file_with_vids", dungeon_regen_file_with_vids }, + { "regen_file_with_immunity", dungeon_regen_file_with_immunity }, + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions { "set_regen_file", dungeon_set_regen_file }, { "clear_regen", dungeon_clear_regen }, { "set_exit_all_at_eliminate", dungeon_set_exit_all_at_eliminate}, diff --git a/src/game/questlua_npc.cpp b/src/game/questlua_npc.cpp index 678c85e..11d65b3 100644 --- a/src/game/questlua_npc.cpp +++ b/src/game/questlua_npc.cpp @@ -8,6 +8,7 @@ #include "char_manager.h" #include "shop_manager.h" #include "guild.h" +#include "mob_manager.h" namespace quest { @@ -350,6 +351,214 @@ namespace quest return 0; } + // MR-8: Damage Immunity System - Lua Bindings + int npc_set_damage_immunity(lua_State* L) + { + // npc.set_damage_immunity(vid, immune_bool) + // Sets basic immunity on/off for a specific VID + + if (!lua_isnumber(L, 1)) + { + lua_pushboolean(L, false); + return 1; + } + + DWORD dwVID = (DWORD)lua_tonumber(L, 1); + bool bImmune = lua_toboolean(L, 2); + + LPCHARACTER ch = CHARACTER_MANAGER::instance().Find(dwVID); + if (!ch) + { + sys_err("npc.set_damage_immunity: VID %u not found", dwVID); + lua_pushboolean(L, false); + return 1; + } + + if (!ch->IsMonster() && !ch->IsStone() && !ch->IsDoor()) + { + sys_err("npc.set_damage_immunity: VID %u is not a monster/stone/door", dwVID); + lua_pushboolean(L, false); + return 1; + } + + ch->SetDamageImmunity(bImmune); + lua_pushboolean(L, true); + return 1; + } + + int npc_is_damage_immune(lua_State* L) + { + // npc.is_damage_immune(vid) + // Checks if a VID has damage immunity enabled + + if (!lua_isnumber(L, 1)) + { + lua_pushboolean(L, false); + return 1; + } + + LPCHARACTER ch = CHARACTER_MANAGER::instance().Find((DWORD)lua_tonumber(L, 1)); + lua_pushboolean(L, ch ? ch->IsDamageImmune() : false); + return 1; + } + + int npc_add_damage_immunity_condition(lua_State* L) + { + // npc.add_damage_immunity_condition(vid, condition_type, value, [extra_string]) + // Adds a condition that must be met for attacker to damage this entity + // Types: 0=affect, 1=level_min, 2=level_max, 3=quest_flag, 4=item_equipped, 5=empire, 6=job + + if (!lua_isnumber(L, 1) || !lua_isnumber(L, 2) || !lua_isnumber(L, 3)) + { + lua_pushboolean(L, false); + return 1; + } + + DWORD dwVID = (DWORD)lua_tonumber(L, 1); + BYTE bType = (BYTE)lua_tonumber(L, 2); + DWORD dwValue = (DWORD)lua_tonumber(L, 3); + std::string strExtra = lua_isstring(L, 4) ? lua_tostring(L, 4) : ""; + + LPCHARACTER ch = CHARACTER_MANAGER::instance().Find(dwVID); + if (!ch) + { + sys_err("npc.add_damage_immunity_condition: VID %u not found", dwVID); + lua_pushboolean(L, false); + return 1; + } + + if (!ch->IsMonster() && !ch->IsStone() && !ch->IsDoor()) + { + sys_err("npc.add_damage_immunity_condition: VID %u is not a monster/stone/door", dwVID); + lua_pushboolean(L, false); + return 1; + } + + ch->AddDamageImmunityCondition(bType, dwValue, strExtra); + lua_pushboolean(L, true); + return 1; + } + + int npc_clear_damage_immunity_conditions(lua_State* L) + { + // npc.clear_damage_immunity_conditions(vid) + // Removes all damage immunity conditions from a VID + + if (!lua_isnumber(L, 1)) + { + lua_pushboolean(L, false); + return 1; + } + + LPCHARACTER ch = CHARACTER_MANAGER::instance().Find((DWORD)lua_tonumber(L, 1)); + if (!ch) + { + lua_pushboolean(L, false); + return 1; + } + + ch->ClearDamageImmunityConditions(); + lua_pushboolean(L, true); + return 1; + } + + int npc_set_damage_immunity_with_conditions(lua_State* L) + { + // npc.set_damage_immunity_with_conditions(vid, conditions_table) + // High-level function that sets immunity and conditions in one call + // Example: npc.set_damage_immunity_with_conditions(vid, { + // {type=0, value=23}, -- Need affect 23 + // {type=1, value=50}, -- Need level >= 50 + // {type=3, value=1, extra="dungeon.flag"} -- Need quest flag + // }) + + if (!lua_isnumber(L, 1) || !lua_istable(L, 2)) + { + lua_pushboolean(L, false); + return 1; + } + + DWORD dwVID = (DWORD)lua_tonumber(L, 1); + LPCHARACTER ch = CHARACTER_MANAGER::instance().Find(dwVID); + + if (!ch) + { + sys_err("npc.set_damage_immunity_with_conditions: VID %u not found", dwVID); + lua_pushboolean(L, false); + + return 1; + } + + if (!ch->IsMonster() && !ch->IsStone() && !ch->IsDoor()) + { + sys_err("npc.set_damage_immunity_with_conditions: VID %u is not a monster/stone/door", dwVID); + lua_pushboolean(L, false); + + return 1; + } + + // CRITICAL: Set immunity flag FIRST to close the race condition window + // This ensures the mob is protected immediately, even before conditions are parsed + ch->SetDamageImmunity(true); + + // Clear existing conditions + ch->ClearDamageImmunityConditions(); + + // Parse conditions table + int condCount = 0; + + lua_pushnil(L); + + while (lua_next(L, 2) != 0) + { + if (lua_istable(L, -1)) + { + BYTE bType = 0; + DWORD dwValue = 0; + std::string strExtra = ""; + + // Get 'type' field + lua_pushstring(L, "type"); + lua_gettable(L, -2); + + if (lua_isnumber(L, -1)) + bType = (BYTE)lua_tonumber(L, -1); + + lua_pop(L, 1); + + // Get 'value' field + lua_pushstring(L, "value"); + lua_gettable(L, -2); + + if (lua_isnumber(L, -1)) + dwValue = (DWORD)lua_tonumber(L, -1); + lua_pop(L, 1); + + // Get 'extra' field (optional) + lua_pushstring(L, "extra"); + lua_gettable(L, -2); + + if (lua_isstring(L, -1)) + strExtra = lua_tostring(L, -1); + + lua_pop(L, 1); + + ch->AddDamageImmunityCondition(bType, dwValue, strExtra); + condCount++; + } + + lua_pop(L, 1); + } + + // Note: Immunity flag was already set at the start to minimize race condition + // If no conditions were added, the mob will block ALL damage (fail-safe) + + lua_pushboolean(L, true); + + return 1; + } + // MR-8: -- END OF -- Damage Immunity System - Lua Bindings + void RegisterNPCFunctionTable() { luaL_reg npc_functions[] = @@ -379,6 +588,15 @@ namespace quest { "dec_remain_skill_book_count", npc_dec_remain_skill_book_count }, { "get_remain_hairdye_count", npc_get_remain_hairdye_count }, { "dec_remain_hairdye_count", npc_dec_remain_hairdye_count }, + + // MR-8: Damage Immunity System - Lua Bindings + { "set_damage_immunity", npc_set_damage_immunity }, + { "is_damage_immune", npc_is_damage_immune }, + { "add_damage_immunity_condition", npc_add_damage_immunity_condition }, + { "clear_damage_immunity_conditions", npc_clear_damage_immunity_conditions }, + { "set_damage_immunity_with_conditions", npc_set_damage_immunity_with_conditions }, + // MR-8: -- END OF -- Damage Immunity System - Lua Bindings + { NULL, NULL } }; diff --git a/src/game/questlua_pc.cpp b/src/game/questlua_pc.cpp index 6b08bac..4af12e8 100644 --- a/src/game/questlua_pc.cpp +++ b/src/game/questlua_pc.cpp @@ -266,7 +266,8 @@ namespace quest int pc_in_dungeon(lua_State * L) { LPCHARACTER ch = CQuestManager::instance().GetCurrentCharacterPtr(); - lua_pushboolean(L, ch->GetDungeon()?1:0); + lua_pushboolean(L, ch->GetDungeon() ? 1 : 0); + return 1; } @@ -1245,6 +1246,18 @@ namespace quest if (!lua_isnumber(L, 1)) return 0; + LPCHARACTER ch = CQuestManager::instance().GetCurrentCharacterPtr(); + + // MR-8: Prevent mounting in Nemere's Watchtower + long lMapIndex = ch->GetMapIndex(); + + if (lMapIndex >= 352 * 10000 && lMapIndex < 353 * 10000) + { + ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("You cannot ride your horse in Nemere's Watchtower.")); + return 0; + } + // MR-8: -- END OF -- Prevent mounting in Nemere's Watchtower + int length = 60; if (lua_isnumber(L, 2)) @@ -1255,8 +1268,6 @@ namespace quest if (length < 0) length = 60; - LPCHARACTER ch = CQuestManager::instance().GetCurrentCharacterPtr(); - ch->RemoveAffect(AFFECT_MOUNT); ch->RemoveAffect(AFFECT_MOUNT_BONUS); @@ -1310,11 +1321,16 @@ namespace quest LPCHARACTER ch = CQuestManager::instance().GetCurrentCharacterPtr(); - if( NULL != ch ) + // MR-8: Prevent mount bonuses in Nemere's Watchtower + long lMapIndex = ch->GetMapIndex(); + bool isInNemereDungeon = lMapIndex >= 352 * 10000 && lMapIndex < 353 * 10000; + + if (NULL != ch && !isInNemereDungeon) { ch->RemoveAffect(AFFECT_MOUNT_BONUS); ch->AddAffect(AFFECT_MOUNT_BONUS, aApplyInfo[applyOn].bPointType, value, AFF_NONE, duration, 0, false); } + // MR-8: -- END OF -- Prevent mount bonuses in Nemere's Watchtower return 0; } @@ -1322,10 +1338,13 @@ namespace quest int pc_unmount(lua_State* L) { LPCHARACTER ch = CQuestManager::instance().GetCurrentCharacterPtr(); + ch->RemoveAffect(AFFECT_MOUNT); ch->RemoveAffect(AFFECT_MOUNT_BONUS); + if (ch->IsHorseRiding()) ch->StopRiding(); + return 0; } diff --git a/src/game/regen.cpp b/src/game/regen.cpp index 4106907..e5d7512 100644 --- a/src/game/regen.cpp +++ b/src/game/regen.cpp @@ -253,7 +253,9 @@ bool is_regen_exception(long x, long y) return false; } -static void regen_spawn_dungeon(LPREGEN regen, LPDUNGEON pDungeon, bool bOnce) +// MR-8: Snow dungeon - All-damage immunity with exceptions +static void regen_spawn_dungeon(LPREGEN regen, LPDUNGEON pDungeon, bool bOnce, std::vector* pOutVids, const std::vector* pConditions) +// MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions { DWORD num; DWORD i; @@ -275,6 +277,20 @@ static void regen_spawn_dungeon(LPREGEN regen, LPDUNGEON pDungeon, bool bOnce) { ++regen->count; ch->SetDungeon(pDungeon); + + // MR-8: Snow dungeon - All-damage immunity with exceptions + // Apply damage immunity atomically before mob is attackable + if (pConditions && !pConditions->empty()) + { + ch->SetDamageImmunity(true); + + for (const auto& cond : *pConditions) + ch->AddDamageImmunityCondition(cond.bType, cond.dwValue, cond.strExtra); + } + + if (pOutVids) + pOutVids->push_back(ch->GetVID()); + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions } } else if (regen->sx == regen->ex && regen->sy == regen->ey) @@ -291,6 +307,20 @@ static void regen_spawn_dungeon(LPREGEN regen, LPDUNGEON pDungeon, bool bOnce) { ++regen->count; ch->SetDungeon(pDungeon); + + // MR-8: Snow dungeon - All-damage immunity with exceptions + // Apply damage immunity atomically before mob is attackable + if (pConditions && !pConditions->empty()) + { + ch->SetDamageImmunity(true); + + for (const auto& cond : *pConditions) + ch->AddDamageImmunityCondition(cond.bType, cond.dwValue, cond.strExtra); + } + + if (pOutVids) + pOutVids->push_back(ch->GetVID()); + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions } } else @@ -303,12 +333,53 @@ static void regen_spawn_dungeon(LPREGEN regen, LPDUNGEON pDungeon, bool bOnce) { ++regen->count; ch->SetDungeon(pDungeon); + + // MR-8: Snow dungeon - All-damage immunity with exceptions + // Apply damage immunity atomically before mob is attackable + if (pConditions && !pConditions->empty()) + { + ch->SetDamageImmunity(true); + + for (const auto& cond : *pConditions) + ch->AddDamageImmunityCondition(cond.bType, cond.dwValue, cond.strExtra); + } + + if (pOutVids) + pOutVids->push_back(ch->GetVID()); + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions } } else if (regen->type == REGEN_TYPE_GROUP) { - if (CHARACTER_MANAGER::Instance().SpawnGroup(regen->vnum, regen->lMapIndex, regen->sx, regen->sy, regen->ex, regen->ey, bOnce ? NULL : regen, regen->is_aggressive, pDungeon)) + // MR-8: Snow dungeon - All-damage immunity with exceptions + std::vector localVids; + LPCHARACTER leader = CHARACTER_MANAGER::Instance().SpawnGroupWithVIDs(regen->vnum, regen->lMapIndex, regen->sx, regen->sy, regen->ex, regen->ey, bOnce ? NULL : regen, regen->is_aggressive, pDungeon, localVids); + + if (leader) + { ++regen->count; + + // Apply damage immunity to all spawned group members if requested. + if (pConditions && !pConditions->empty()) + { + for (DWORD vid : localVids) + { + LPCHARACTER member = CHARACTER_MANAGER::instance().Find(vid); + + if (!member) + continue; + + member->SetDamageImmunity(true); + + for (const auto& cond : *pConditions) + member->AddDamageImmunityCondition(cond.bType, cond.dwValue, cond.strExtra); + } + } + + if (pOutVids) + pOutVids->insert(pOutVids->end(), localVids.begin(), localVids.end()); + } + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions } else if (regen->type == REGEN_TYPE_GROUP_GROUP) { @@ -403,11 +474,15 @@ EVENTFUNC(dungeon_regen_event) regen->event = NULL; } - regen_spawn_dungeon(regen, pDungeon, false); + // MR-8: Snow dungeon - All-damage immunity with exceptions + regen_spawn_dungeon(regen, pDungeon, false, NULL, NULL); + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions return PASSES_PER_SEC(regen->time); } -bool regen_do(const char* filename, long lMapIndex, int base_x, int base_y, LPDUNGEON pDungeon, bool bOnce) +// MR-8:Snow dungeon - All-damage immunity with exceptions +bool regen_do(const char* filename, long lMapIndex, int base_x, int base_y, LPDUNGEON pDungeon, bool bOnce, std::vector* pOutVids, const std::vector* pConditions) +// MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions { if (g_bNoRegen) return true; @@ -500,8 +575,10 @@ bool regen_do(const char* filename, long lMapIndex, int base_x, int base_y, LPDU // before the call to CHARACTER::SetRegen() } - // ์ฒ˜์Œ์—” ๋ฌด์กฐ๊ฑด ๋ฆฌ์   ํ•ด์ค€๋‹ค. - regen_spawn_dungeon(regen, pDungeon, bOnce); + // MR-8: Snow dungeon - All-damage immunity with exceptions + // ์ฒ˜์Œ์—” ๋ฌด์กฐ๊ฑด ๋ฆฌ์   ํ•ด์ค€๋‹ค. Optionally collect VIDs for the initial spawn. + regen_spawn_dungeon(regen, pDungeon, bOnce, pOutVids, pConditions); + // MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions } } diff --git a/src/game/regen.h b/src/game/regen.h index c484dd2..170b07e 100644 --- a/src/game/regen.h +++ b/src/game/regen.h @@ -1,4 +1,10 @@ -๏ปฟ#include "dungeon.h" +๏ปฟ#include +#include "dungeon.h" + +// MR-8: Snow dungeon - All-damage immunity with exceptions +// Forward declaration +class CHARACTER; +// MR-8: -- END OF -- Snow dungeon - All-damage immunity with exceptions enum { @@ -84,7 +90,9 @@ EVENTINFO(dungeon_regen_event_info) }; extern bool regen_load(const char *filename, long lMapIndex, int base_x, int base_y); -extern bool regen_do(const char* filename, long lMapIndex, int base_x, int base_y, LPDUNGEON pDungeon, bool bOnce = true ); +// MR-8: Snow dungeon - All-damage immunity with exceptions +extern bool regen_do(const char* filename, long lMapIndex, int base_x, int base_y, LPDUNGEON pDungeon, bool bOnce = true, std::vector* pOutVids = NULL, const std::vector* pConditions = NULL); +// MR-8: --END OF -- Snow dungeon - All-damage immunity with exceptions extern bool regen_load_in_file(const char* filename, long lMapIndex, int base_x, int base_y ); extern void regen_free();