diff options
author | Traesh <Traesh@users.noreply.github.com> | 2018-07-31 23:31:44 +0200 |
---|---|---|
committer | Shauren <shauren.trinity@gmail.com> | 2018-07-31 23:31:44 +0200 |
commit | 55a0a2d3f6146ace93d5558ce16469d3cca28bdc (patch) | |
tree | d1b61e149fcb44c0c153c3ed614e42640a81df28 | |
parent | 394f9775a7c6d597c9744db37783985b2e228572 (diff) |
Core/Artifact Handle Artifact tiers + Artifact unlock (3rd relic bonus) (#21593)
25 files changed, 320 insertions, 58 deletions
diff --git a/sql/base/characters_database.sql b/sql/base/characters_database.sql index ef73caef3aa..687f66af959 100644 --- a/sql/base/characters_database.sql +++ b/sql/base/characters_database.sql @@ -2821,6 +2821,7 @@ CREATE TABLE `item_instance_artifact` ( `itemGuid` bigint(20) unsigned NOT NULL, `xp` bigint(20) unsigned NOT NULL DEFAULT '0', `artifactAppearanceId` int(10) unsigned NOT NULL DEFAULT '0', + `artifactTierId` int(10) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`itemGuid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; @@ -3564,7 +3565,8 @@ INSERT INTO `updates` VALUES ('2018_02_19_00_characters.sql','75A0FFAFD0633921708DB0F72F9CC9796ACB960B','RELEASED','2018-02-19 22:33:32',117), ('2018_03_04_00_characters.sql','2A4CD2EE2547E718490706FADC78BF36F0DED8D6','RELEASED','2018-03-04 18:15:24',0), ('2018_04_28_00_characters.sql','CBD0FDC0F32DE3F456F7CE3D9CAD6933CD6A50F5','RELEASED','2018-04-28 12:44:09',0), -('2018_07_28_00_characters.sql','31F66AE7831251A8915625EC7F10FA138AB8B654','RELEASED','2018-07-28 18:30:19',0); +('2018_07_28_00_characters.sql','31F66AE7831251A8915625EC7F10FA138AB8B654','RELEASED','2018-07-28 18:30:19',0), +('2018_07_31_00_characters.sql','7DA8D4A4534520B23E6F5BBD5B8EE205B799C798','RELEASED','2018-07-31 20:54:39',0); /*!40000 ALTER TABLE `updates` ENABLE KEYS */; UNLOCK TABLES; diff --git a/sql/updates/characters/master/2018_07_31_00_characters.sql b/sql/updates/characters/master/2018_07_31_00_characters.sql new file mode 100644 index 00000000000..fed2be26b77 --- /dev/null +++ b/sql/updates/characters/master/2018_07_31_00_characters.sql @@ -0,0 +1 @@ +ALTER TABLE `item_instance_artifact` ADD `artifactTierId` int(10) unsigned NOT NULL DEFAULT '0' AFTER `artifactAppearanceId`; diff --git a/sql/updates/hotfixes/master/2018_03_18_00_hotfixes.sql b/sql/updates/hotfixes/master/2018_03_18_00_hotfixes.sql new file mode 100644 index 00000000000..d4271bb2523 --- /dev/null +++ b/sql/updates/hotfixes/master/2018_03_18_00_hotfixes.sql @@ -0,0 +1,29 @@ +-- +-- Table structure for table `artifact_tier` +-- +DROP TABLE IF EXISTS `artifact_tier`; +CREATE TABLE `artifact_tier` ( + `ID` int(10) unsigned NOT NULL DEFAULT '0', + `ArtifactTier` int(10) unsigned NOT NULL DEFAULT '0', + `MaxNumTraits` int(10) unsigned NOT NULL DEFAULT '0', + `MaxArtifactKnowledge` int(10) unsigned NOT NULL DEFAULT '0', + `KnowledgePlayerCondition` int(10) unsigned NOT NULL DEFAULT '0', + `MinimumEmpowerKnowledge` int(10) unsigned NOT NULL DEFAULT '0', + `VerifiedBuild` smallint(6) NOT NULL DEFAULT '0', + PRIMARY KEY (`ID`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- +-- Table structure for table `artifact_unlock` +-- +DROP TABLE IF EXISTS `artifact_unlock`; +CREATE TABLE `artifact_unlock` ( + `ID` int(10) unsigned NOT NULL DEFAULT '0', + `ItemBonusListID` smallint(5) unsigned NOT NULL DEFAULT '0', + `PowerRank` tinyint(3) unsigned NOT NULL DEFAULT '0', + `PowerID` int(10) unsigned NOT NULL DEFAULT '0', + `PlayerConditionID` int(10) unsigned NOT NULL DEFAULT '0', + `ArtifactID` tinyint(3) unsigned NOT NULL DEFAULT '0', + `VerifiedBuild` smallint(6) NOT NULL DEFAULT '0', + PRIMARY KEY (`ID`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; diff --git a/src/server/database/Database/Implementation/CharacterDatabase.cpp b/src/server/database/Database/Implementation/CharacterDatabase.cpp index 3590fea0414..d0227fde660 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.cpp +++ b/src/server/database/Database/Implementation/CharacterDatabase.cpp @@ -182,8 +182,8 @@ void CharacterDatabaseConnection::DoPrepareStatements() "spellItemEnchantmentAllSpecs, spellItemEnchantmentSpec1, spellItemEnchantmentSpec2, spellItemEnchantmentSpec3, spellItemEnchantmentSpec4) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC); PrepareStatement(CHAR_DEL_ITEM_INSTANCE_TRANSMOG, "DELETE FROM item_instance_transmog WHERE itemGuid = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_DEL_ITEM_INSTANCE_TRANSMOG_BY_OWNER, "DELETE iit FROM item_instance_transmog iit LEFT JOIN item_instance ii ON iit.itemGuid = ii.guid WHERE ii.owner_guid = ?", CONNECTION_ASYNC); - PrepareStatement(CHAR_SEL_ITEM_INSTANCE_ARTIFACT, "SELECT a.itemGuid, a.xp, a.artifactAppearanceId, ap.artifactPowerId, ap.purchasedRank FROM item_instance_artifact_powers ap LEFT JOIN item_instance_artifact a ON ap.itemGuid = a.itemGuid INNER JOIN character_inventory ci ON ci.item = ap.itemGuid WHERE ci.guid = ?", CONNECTION_ASYNC); - PrepareStatement(CHAR_INS_ITEM_INSTANCE_ARTIFACT, "INSERT INTO item_instance_artifact (itemGuid, xp, artifactAppearanceId) VALUES (?, ?, ?)", CONNECTION_ASYNC); + PrepareStatement(CHAR_SEL_ITEM_INSTANCE_ARTIFACT, "SELECT a.itemGuid, a.xp, a.artifactAppearanceId, a.artifactTierId, ap.artifactPowerId, ap.purchasedRank FROM item_instance_artifact_powers ap LEFT JOIN item_instance_artifact a ON ap.itemGuid = a.itemGuid INNER JOIN character_inventory ci ON ci.item = ap.itemGuid WHERE ci.guid = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_INS_ITEM_INSTANCE_ARTIFACT, "INSERT INTO item_instance_artifact (itemGuid, xp, artifactAppearanceId, artifactTierId) VALUES (?, ?, ?, ?)", CONNECTION_ASYNC); PrepareStatement(CHAR_DEL_ITEM_INSTANCE_ARTIFACT, "DELETE FROM item_instance_artifact WHERE itemGuid = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_DEL_ITEM_INSTANCE_ARTIFACT_BY_OWNER, "DELETE iia FROM item_instance_artifact iia LEFT JOIN item_instance ii ON iia.itemGuid = ii.guid WHERE ii.owner_guid = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_INS_ITEM_INSTANCE_ARTIFACT_POWERS, "INSERT INTO item_instance_artifact_powers (itemGuid, artifactPowerId, purchasedRank) VALUES (?, ?, ?)", CONNECTION_ASYNC); diff --git a/src/server/database/Database/Implementation/HotfixDatabase.cpp b/src/server/database/Database/Implementation/HotfixDatabase.cpp index 23772fa2e57..8c1bb487363 100644 --- a/src/server/database/Database/Implementation/HotfixDatabase.cpp +++ b/src/server/database/Database/Implementation/HotfixDatabase.cpp @@ -95,6 +95,14 @@ void HotfixDatabaseConnection::DoPrepareStatements() PrepareStatement(HOTFIX_SEL_ARTIFACT_QUEST_XP, "SELECT ID, Difficulty1, Difficulty2, Difficulty3, Difficulty4, Difficulty5, Difficulty6, " "Difficulty7, Difficulty8, Difficulty9, Difficulty10 FROM artifact_quest_xp ORDER BY ID DESC", CONNECTION_SYNCH); + // ArtifactTier.db2 + PrepareStatement(HOTFIX_SEL_ARTIFACT_TIER, "SELECT ID, ArtifactTier, MaxNumTraits, MaxArtifactKnowledge, KnowledgePlayerCondition, " + "MinimumEmpowerKnowledge FROM artifact_tier ORDER BY ID DESC", CONNECTION_SYNCH); + + // ArtifactUnlock.db2 + PrepareStatement(HOTFIX_SEL_ARTIFACT_UNLOCK, "SELECT ID, ItemBonusListID, PowerRank, PowerID, PlayerConditionID, ArtifactID FROM artifact_unlock" + " ORDER BY ID DESC", CONNECTION_SYNCH); + // AuctionHouse.db2 PrepareStatement(HOTFIX_SEL_AUCTION_HOUSE, "SELECT ID, Name, FactionID, DepositRate, ConsignmentRate FROM auction_house ORDER BY ID DESC", CONNECTION_SYNCH); PREPARE_LOCALE_STMT(HOTFIX_SEL_AUCTION_HOUSE, "SELECT ID, Name_lang FROM auction_house_locale WHERE locale = ?", CONNECTION_SYNCH); diff --git a/src/server/database/Database/Implementation/HotfixDatabase.h b/src/server/database/Database/Implementation/HotfixDatabase.h index 5f15a3adea1..77f3600ff39 100644 --- a/src/server/database/Database/Implementation/HotfixDatabase.h +++ b/src/server/database/Database/Implementation/HotfixDatabase.h @@ -66,6 +66,10 @@ enum HotfixDatabaseStatements : uint32 HOTFIX_SEL_ARTIFACT_QUEST_XP, + HOTFIX_SEL_ARTIFACT_TIER, + + HOTFIX_SEL_ARTIFACT_UNLOCK, + HOTFIX_SEL_AUCTION_HOUSE, HOTFIX_SEL_AUCTION_HOUSE_LOCALE, diff --git a/src/server/game/DataStores/DB2LoadInfo.h b/src/server/game/DataStores/DB2LoadInfo.h index 5560141fd7b..a6b4c1c0426 100644 --- a/src/server/game/DataStores/DB2LoadInfo.h +++ b/src/server/game/DataStores/DB2LoadInfo.h @@ -345,6 +345,42 @@ struct ArtifactQuestXpLoadInfo } }; +struct ArtifactTierLoadInfo +{ + static DB2LoadInfo const* Instance() + { + static DB2FieldMeta const fields[] = + { + { false, FT_INT, "ID" }, + { false, FT_INT, "ArtifactTier" }, + { false, FT_INT, "MaxNumTraits" }, + { false, FT_INT, "MaxArtifactKnowledge" }, + { false, FT_INT, "KnowledgePlayerCondition" }, + { false, FT_INT, "MinimumEmpowerKnowledge" }, + }; + static DB2LoadInfo const loadInfo(&fields[0], std::extent<decltype(fields)>::value, ArtifactTierMeta::Instance(), HOTFIX_SEL_ARTIFACT_TIER); + return &loadInfo; + } +}; + +struct ArtifactUnlockLoadInfo +{ + static DB2LoadInfo const* Instance() + { + static DB2FieldMeta const fields[] = + { + { false, FT_INT, "ID" }, + { false, FT_SHORT, "ItemBonusListID" }, + { false, FT_BYTE, "PowerRank" }, + { false, FT_INT, "PowerID" }, + { false, FT_INT, "PlayerConditionID" }, + { false, FT_BYTE, "ArtifactID" }, + }; + static DB2LoadInfo const loadInfo(&fields[0], std::extent<decltype(fields)>::value, ArtifactUnlockMeta::Instance(), HOTFIX_SEL_ARTIFACT_UNLOCK); + return &loadInfo; + } +}; + struct AuctionHouseLoadInfo { static DB2LoadInfo const* Instance() diff --git a/src/server/game/DataStores/DB2Stores.cpp b/src/server/game/DataStores/DB2Stores.cpp index 7faae631eb9..fe91d679d97 100644 --- a/src/server/game/DataStores/DB2Stores.cpp +++ b/src/server/game/DataStores/DB2Stores.cpp @@ -50,6 +50,8 @@ DB2Storage<ArtifactPowerLinkEntry> sArtifactPowerLinkStore("Artifac DB2Storage<ArtifactPowerPickerEntry> sArtifactPowerPickerStore("ArtifactPowerPicker.db2", ArtifactPowerPickerLoadInfo::Instance()); DB2Storage<ArtifactPowerRankEntry> sArtifactPowerRankStore("ArtifactPowerRank.db2", ArtifactPowerRankLoadInfo::Instance()); DB2Storage<ArtifactQuestXPEntry> sArtifactQuestXPStore("ArtifactQuestXP.db2", ArtifactQuestXpLoadInfo::Instance()); +DB2Storage<ArtifactTierEntry> sArtifactTierStore("ArtifactTier.db2", ArtifactTierLoadInfo::Instance()); +DB2Storage<ArtifactUnlockEntry> sArtifactUnlockStore("ArtifactUnlock.db2", ArtifactUnlockLoadInfo::Instance()); DB2Storage<AuctionHouseEntry> sAuctionHouseStore("AuctionHouse.db2", AuctionHouseLoadInfo::Instance()); DB2Storage<BankBagSlotPricesEntry> sBankBagSlotPricesStore("BankBagSlotPrices.db2", BankBagSlotPricesLoadInfo::Instance()); DB2Storage<BannedAddonsEntry> sBannedAddonsStore("BannedAddons.db2", BannedAddonsLoadInfo::Instance()); @@ -488,6 +490,8 @@ void DB2Manager::LoadStores(std::string const& dataPath, uint32 defaultLocale) LOAD_DB2(sArtifactPowerLinkStore); LOAD_DB2(sArtifactPowerPickerStore); LOAD_DB2(sArtifactPowerRankStore); + LOAD_DB2(sArtifactTierStore); + LOAD_DB2(sArtifactUnlockStore); LOAD_DB2(sAuctionHouseStore); LOAD_DB2(sBankBagSlotPricesStore); LOAD_DB2(sBannedAddonsStore); diff --git a/src/server/game/DataStores/DB2Stores.h b/src/server/game/DataStores/DB2Stores.h index a7b070d5492..d5f0af253a0 100644 --- a/src/server/game/DataStores/DB2Stores.h +++ b/src/server/game/DataStores/DB2Stores.h @@ -45,6 +45,8 @@ TC_GAME_API extern DB2Storage<ArtifactAppearanceEntry> sArtifactApp TC_GAME_API extern DB2Storage<ArtifactAppearanceSetEntry> sArtifactAppearanceSetStore; TC_GAME_API extern DB2Storage<ArtifactPowerEntry> sArtifactPowerStore; TC_GAME_API extern DB2Storage<ArtifactPowerPickerEntry> sArtifactPowerPickerStore; +TC_GAME_API extern DB2Storage<ArtifactTierEntry> sArtifactTierStore; +TC_GAME_API extern DB2Storage<ArtifactUnlockEntry> sArtifactUnlockStore; TC_GAME_API extern DB2Storage<AuctionHouseEntry> sAuctionHouseStore; TC_GAME_API extern DB2Storage<BankBagSlotPricesEntry> sBankBagSlotPricesStore; TC_GAME_API extern DB2Storage<BannedAddonsEntry> sBannedAddonsStore; diff --git a/src/server/game/DataStores/DB2Structure.h b/src/server/game/DataStores/DB2Structure.h index 639c7b66877..e5f46417a81 100644 --- a/src/server/game/DataStores/DB2Structure.h +++ b/src/server/game/DataStores/DB2Structure.h @@ -219,6 +219,26 @@ struct ArtifactQuestXPEntry uint32 Difficulty[10]; }; +struct ArtifactTierEntry +{ + uint32 ID; + uint32 ArtifactTier; + uint32 MaxNumTraits; + uint32 MaxArtifactKnowledge; + uint32 KnowledgePlayerCondition; + uint32 MinimumEmpowerKnowledge; +}; + +struct ArtifactUnlockEntry +{ + uint32 ID; + uint16 ItemBonusListID; + uint8 PowerRank; + uint32 PowerID; + uint32 PlayerConditionID; + uint8 ArtifactID; +}; + struct AuctionHouseEntry { uint32 ID; diff --git a/src/server/game/DataStores/DBCEnums.h b/src/server/game/DataStores/DBCEnums.h index 7807bb5538e..80d109a51a1 100644 --- a/src/server/game/DataStores/DBCEnums.h +++ b/src/server/game/DataStores/DBCEnums.h @@ -146,15 +146,26 @@ enum AreaMountFlags AREA_MOUNT_FLAG_UNDERWATER_ALLOWED = 0x8 }; +enum ArtifactCategory : uint32 +{ + ARTIFACT_CATEGORY_PRIMARY = 1, + ARTIFACT_CATEGORY_FISHING = 2 +}; + enum ArtifactPowerFlag : uint8 { ARTIFACT_POWER_FLAG_GOLD = 0x01, - ARTIFACT_POWER_FLAG_FIRST = 0x02, + ARTIFACT_POWER_FLAG_NO_LINK_REQUIRED = 0x02, ARTIFACT_POWER_FLAG_FINAL = 0x04, ARTIFACT_POWER_FLAG_SCALES_WITH_NUM_POWERS = 0x08, ARTIFACT_POWER_FLAG_DONT_COUNT_FIRST_BONUS_RANK = 0x10, + ARTIFACT_POWER_FLAG_MAX_RANK_WITH_TIER = 0x20, + + ARTIFACT_POWER_FLAG_FIRST = ARTIFACT_POWER_FLAG_NO_LINK_REQUIRED | ARTIFACT_POWER_FLAG_DONT_COUNT_FIRST_BONUS_RANK, }; +#define MAX_ARTIFACT_TIER 1 + #define BATTLE_PET_SPECIES_MAX_ID 2164 enum ChrSpecializationFlag @@ -1050,7 +1061,6 @@ enum CurrencyTypes CURRENCY_TYPE_JUSTICE_POINTS = 395, CURRENCY_TYPE_VALOR_POINTS = 396, CURRENCY_TYPE_APEXIS_CRYSTALS = 823, - CURRENCY_TYPE_ARTIFACT_KNOWLEDGE = 1171, }; enum WorldMapTransformsFlags diff --git a/src/server/game/Entities/Item/Item.cpp b/src/server/game/Entities/Item/Item.cpp index 71bc71f9430..95c4b0f4ac2 100644 --- a/src/server/game/Entities/Item/Item.cpp +++ b/src/server/game/Entities/Item/Item.cpp @@ -320,7 +320,7 @@ bool Item::Create(ObjectGuid::LowType guidlow, uint32 itemId, Player const* owne SetSpellCharges(i, itemProto->Effects[i]->Charges); if (SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(itemProto->Effects[i]->SpellID)) if (owner && spellInfo->HasEffect(SPELL_EFFECT_GIVE_ARTIFACT_POWER)) - if (uint32 artifactKnowledgeLevel = owner->GetCurrency(CURRENCY_TYPE_ARTIFACT_KNOWLEDGE)) + if (uint32 artifactKnowledgeLevel = sWorld->getIntConfig(CONFIG_CURRENCY_START_ARTIFACT_KNOWLEDGE)) SetModifier(ITEM_MODIFIER_ARTIFACT_KNOWLEDGE_LEVEL, artifactKnowledgeLevel + 1); } @@ -346,6 +346,8 @@ bool Item::Create(ObjectGuid::LowType guidlow, uint32 itemId, Player const* owne break; } } + + CheckArtifactRelicSlotUnlock(owner ? owner : GetOwner()); } return true; @@ -536,6 +538,7 @@ void Item::SaveToDB(SQLTransaction& trans) stmt->setUInt64(0, GetGUID().GetCounter()); stmt->setUInt64(1, GetUInt64Value(ITEM_FIELD_ARTIFACT_XP)); stmt->setUInt32(2, GetModifier(ITEM_MODIFIER_ARTIFACT_APPEARANCE_ID)); + stmt->setUInt32(3, GetModifier(ITEM_MODIFIER_ARTIFACT_TIER)); trans->Append(stmt); for (ItemDynamicFieldArtifactPowers const& artifactPower : GetArtifactPowers()) @@ -794,11 +797,15 @@ bool Item::LoadFromDB(ObjectGuid::LowType guid, ObjectGuid ownerGuid, Field* fie return true; } -void Item::LoadArtifactData(Player* owner, uint64 xp, uint32 artifactAppearanceId, std::vector<ItemDynamicFieldArtifactPowers>& powers) +void Item::LoadArtifactData(Player* owner, uint64 xp, uint32 artifactAppearanceId, uint32 artifactTier, std::vector<ItemDynamicFieldArtifactPowers>& powers) { - InitArtifactPowers(GetTemplate()->GetArtifactID(), 0); + for (uint8 i = 0; i <= artifactTier; ++i) + InitArtifactPowers(GetTemplate()->GetArtifactID(), i); + SetUInt64Value(ITEM_FIELD_ARTIFACT_XP, xp); SetModifier(ITEM_MODIFIER_ARTIFACT_APPEARANCE_ID, artifactAppearanceId); + SetModifier(ITEM_MODIFIER_ARTIFACT_TIER, artifactTier); + if (ArtifactAppearanceEntry const* artifactAppearance = sArtifactAppearanceStore.LookupEntry(artifactAppearanceId)) SetAppearanceModId(artifactAppearance->ItemAppearanceModifierID); @@ -856,6 +863,23 @@ void Item::LoadArtifactData(Player* owner, uint64 xp, uint32 artifactAppearanceI power.CurrentRankWithBonus = totalPurchasedRanks + 1; SetArtifactPower(&power); } + + CheckArtifactRelicSlotUnlock(owner ? owner : GetOwner()); +} + +void Item::CheckArtifactRelicSlotUnlock(Player const* owner) +{ + if (!owner) + return; + + uint8 artifactId = GetTemplate()->GetArtifactID(); + if (!artifactId) + return; + + for (ArtifactUnlockEntry const* artifactUnlock : sArtifactUnlockStore) + if (artifactUnlock->ArtifactID == artifactId) + if (owner->MeetPlayerCondition(artifactUnlock->PlayerConditionID)) + AddBonuses(artifactUnlock->ItemBonusListID); } /*static*/ @@ -2371,6 +2395,9 @@ uint16 Item::GetVisibleItemVisual(Player const* owner) const void Item::AddBonuses(uint32 bonusListID) { + if (HasDynamicValue(ITEM_DYNAMIC_FIELD_BONUSLIST_IDS, bonusListID)) + return; + if (DB2Manager::ItemBonusList const* bonuses = sDB2Manager.GetItemBonusList(bonusListID)) { AddDynamicValue(ITEM_DYNAMIC_FIELD_BONUSLIST_IDS, bonusListID); @@ -2426,7 +2453,7 @@ void Item::InitArtifactPowers(uint8 artifactId, uint8 artifactTier) memset(&powerData, 0, sizeof(powerData)); powerData.ArtifactPowerId = artifactPower->ID; powerData.PurchasedRank = 0; - powerData.CurrentRankWithBonus = (artifactPower->Flags & ARTIFACT_POWER_FLAG_FIRST) ? 1 : 0; + powerData.CurrentRankWithBonus = (artifactPower->Flags & ARTIFACT_POWER_FLAG_FIRST) == ARTIFACT_POWER_FLAG_FIRST ? 1 : 0; SetArtifactPower(&powerData, true); } } @@ -2534,24 +2561,21 @@ void Item::GiveArtifactXp(uint64 amount, Item* sourceItem, uint32 artifactCatego if (artifactCategoryId) { - if (ArtifactCategoryEntry const* artifactCategory = sArtifactCategoryStore.LookupEntry(artifactCategoryId)) - { - uint32 artifactKnowledgeLevel = 1; - if (sourceItem && sourceItem->GetModifier(ITEM_MODIFIER_ARTIFACT_KNOWLEDGE_LEVEL)) - artifactKnowledgeLevel = sourceItem->GetModifier(ITEM_MODIFIER_ARTIFACT_KNOWLEDGE_LEVEL); - else - artifactKnowledgeLevel = owner->GetCurrency(artifactCategory->XpMultCurrencyID) + 1; - - if (GtArtifactKnowledgeMultiplierEntry const* artifactKnowledge = sArtifactKnowledgeMultiplierGameTable.GetRow(artifactKnowledgeLevel)) - amount = uint64(amount * artifactKnowledge->Multiplier); - - if (amount >= 5000) - amount = 50 * (amount / 50); - else if (amount >= 1000) - amount = 25 * (amount / 25); - else if (amount >= 50) - amount = 5 * (amount / 5); - } + uint32 artifactKnowledgeLevel = 1; + if (sourceItem && sourceItem->GetModifier(ITEM_MODIFIER_ARTIFACT_KNOWLEDGE_LEVEL)) + artifactKnowledgeLevel = sourceItem->GetModifier(ITEM_MODIFIER_ARTIFACT_KNOWLEDGE_LEVEL); + else if (artifactCategoryId == ARTIFACT_CATEGORY_PRIMARY) + artifactKnowledgeLevel = sWorld->getIntConfig(CONFIG_CURRENCY_START_ARTIFACT_KNOWLEDGE) + 1; + + if (GtArtifactKnowledgeMultiplierEntry const* artifactKnowledge = sArtifactKnowledgeMultiplierGameTable.GetRow(artifactKnowledgeLevel)) + amount = uint64(amount * artifactKnowledge->Multiplier); + + if (amount >= 5000) + amount = 50 * (amount / 50); + else if (amount >= 1000) + amount = 25 * (amount / 25); + else if (amount >= 50) + amount = 5 * (amount / 5); } SetUInt64Value(ITEM_FIELD_ARTIFACT_XP, GetUInt64Value(ITEM_FIELD_ARTIFACT_XP) + amount); diff --git a/src/server/game/Entities/Item/Item.h b/src/server/game/Entities/Item/Item.h index d795983b4ed..a51415abcbb 100644 --- a/src/server/game/Entities/Item/Item.h +++ b/src/server/game/Entities/Item/Item.h @@ -155,7 +155,8 @@ class TC_GAME_API Item : public Object bool IsBoundByEnchant() const; virtual void SaveToDB(SQLTransaction& trans); virtual bool LoadFromDB(ObjectGuid::LowType guid, ObjectGuid ownerGuid, Field* fields, uint32 entry); - void LoadArtifactData(Player* owner, uint64 xp, uint32 artifactAppearanceId, std::vector<ItemDynamicFieldArtifactPowers>& powers); // must be called after LoadFromDB to have gems (relics) initialized + void LoadArtifactData(Player* owner, uint64 xp, uint32 artifactAppearanceId, uint32 artifactTier, std::vector<ItemDynamicFieldArtifactPowers>& powers); // must be called after LoadFromDB to have gems (relics) initialized + void CheckArtifactRelicSlotUnlock(Player const* owner); void AddBonuses(uint32 bonusListID); diff --git a/src/server/game/Entities/Object/Object.cpp b/src/server/game/Entities/Object/Object.cpp index 17d767f04e9..36bed0bfd4b 100644 --- a/src/server/game/Entities/Object/Object.cpp +++ b/src/server/game/Entities/Object/Object.cpp @@ -1433,6 +1433,17 @@ uint32 Object::GetDynamicValue(uint16 index, uint16 offset) const return _dynamicValues[index][offset]; } +bool Object::HasDynamicValue(uint16 index, uint32 value) +{ + ASSERT(index < _dynamicValuesCount || PrintIndexError(index, false)); + std::vector<uint32>& values = _dynamicValues[index]; + for (std::size_t i = 0; i < values.size(); ++i) + if (values[i] == value) + return true; + + return false; +} + void Object::AddDynamicValue(uint16 index, uint32 value) { ASSERT(index < _dynamicValuesCount || PrintIndexError(index, false)); diff --git a/src/server/game/Entities/Object/Object.h b/src/server/game/Entities/Object/Object.h index 539cf8fbccf..2ebea15a58d 100644 --- a/src/server/game/Entities/Object/Object.h +++ b/src/server/game/Entities/Object/Object.h @@ -188,6 +188,7 @@ class TC_GAME_API Object std::vector<uint32> const& GetDynamicValues(uint16 index) const; uint32 GetDynamicValue(uint16 index, uint16 offset) const; + bool HasDynamicValue(uint16 index, uint32 value); void AddDynamicValue(uint16 index, uint32 value); void RemoveDynamicValue(uint16 index, uint32 value); void ClearDynamicValue(uint16 index); diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 456cd7d910e..d8b2e46974e 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -2458,6 +2458,10 @@ void Player::GiveLevel(uint8 level) _ApplyAllLevelScaleItemMods(true); // Moved to above SetFullHealth so player will have full health from Heirlooms + if (Aura const* artifactAura = GetAura(ARTIFACTS_ALL_WEAPONS_GENERAL_WEAPON_EQUIPPED_PASSIVE)) + if (Item* artifact = GetItemByGuid(artifactAura->GetCastItemGUID())) + artifact->CheckArtifactRelicSlotUnlock(this); + // Only health and mana are set to maximum. SetFullHealth(); SetFullPower(POWER_MANA); @@ -18604,9 +18608,9 @@ void Player::_LoadInventory(PreparedQueryResult result, PreparedQueryResult arti //NOTE2: the "order by `slot`" is needed because mainhand weapons are (wrongly?) //expected to be equipped before offhand items (@todo fixme) - // 0 1 2 3 4 - // SELECT a.itemGuid, a.xp, a.artifactAppearanceId, ap.artifactPowerId, ap.purchasedRank FROM item_instance_artifact_powers ap LEFT JOIN item_instance_artifact a ON ap.itemGuid = a.itemGuid INNER JOIN character_inventory ci ON ci.item = ap.guid WHERE ci.guid = ? - std::unordered_map<ObjectGuid, std::tuple<uint64, uint32, std::vector<ItemDynamicFieldArtifactPowers>>> artifactData; + // 0 1 2 3 4 5 + // SELECT a.itemGuid, a.xp, a.artifactAppearanceId, a.artifactTierId, ap.artifactPowerId, ap.purchasedRank FROM item_instance_artifact_powers ap LEFT JOIN item_instance_artifact a ON ap.itemGuid = a.itemGuid INNER JOIN character_inventory ci ON ci.item = ap.guid WHERE ci.guid = ? + std::unordered_map<ObjectGuid, std::tuple<uint64, uint32, uint32, std::vector<ItemDynamicFieldArtifactPowers>>> artifactData; if (artifactsResult) { do @@ -18615,17 +18619,23 @@ void Player::_LoadInventory(PreparedQueryResult result, PreparedQueryResult arti auto& artifactDataEntry = artifactData[ObjectGuid::Create<HighGuid::Item>(fields[0].GetUInt64())]; std::get<0>(artifactDataEntry) = fields[1].GetUInt64(); std::get<1>(artifactDataEntry) = fields[2].GetUInt32(); + std::get<2>(artifactDataEntry) = fields[3].GetUInt32(); ItemDynamicFieldArtifactPowers artifactPowerData; - artifactPowerData.ArtifactPowerId = fields[3].GetUInt32(); - artifactPowerData.PurchasedRank = fields[4].GetUInt8(); + artifactPowerData.ArtifactPowerId = fields[4].GetUInt32(); + artifactPowerData.PurchasedRank = fields[5].GetUInt8(); if (ArtifactPowerEntry const* artifactPower = sArtifactPowerStore.LookupEntry(artifactPowerData.ArtifactPowerId)) { - if (artifactPowerData.PurchasedRank > artifactPower->MaxPurchasableRank) - artifactPowerData.PurchasedRank = artifactPower->MaxPurchasableRank; + uint32 maxRank = artifactPower->MaxPurchasableRank; + // allow ARTIFACT_POWER_FLAG_FINAL to overflow maxrank here - needs to be handled in Item::CheckArtifactUnlock (will refund artifact power) + if (artifactPower->Flags & ARTIFACT_POWER_FLAG_MAX_RANK_WITH_TIER && artifactPower->Tier < std::get<2>(artifactDataEntry)) + maxRank += std::get<2>(artifactDataEntry) - artifactPower->Tier; + + if (artifactPowerData.PurchasedRank > maxRank) + artifactPowerData.PurchasedRank = maxRank; - artifactPowerData.CurrentRankWithBonus = (artifactPower->Flags & ARTIFACT_POWER_FLAG_FIRST) ? 1 : 0; + artifactPowerData.CurrentRankWithBonus = (artifactPower->Flags & ARTIFACT_POWER_FLAG_FIRST) == ARTIFACT_POWER_FLAG_FIRST ? 1 : 0; - std::get<2>(artifactDataEntry).push_back(artifactPowerData); + std::get<3>(artifactDataEntry).push_back(artifactPowerData); } } while (artifactsResult->NextRow()); @@ -18649,7 +18659,7 @@ void Player::_LoadInventory(PreparedQueryResult result, PreparedQueryResult arti { auto artifactDataItr = artifactData.find(item->GetGUID()); if (item->GetTemplate()->GetArtifactID() && artifactDataItr != artifactData.end()) - item->LoadArtifactData(this, std::get<0>(artifactDataItr->second), std::get<1>(artifactDataItr->second), std::get<2>(artifactDataItr->second)); + item->LoadArtifactData(this, std::get<0>(artifactDataItr->second), std::get<1>(artifactDataItr->second), std::get<2>(artifactDataItr->second), std::get<3>(artifactDataItr->second)); ObjectGuid bagGuid = fields[45].GetUInt64() ? ObjectGuid::Create<HighGuid::Item>(fields[45].GetUInt64()) : ObjectGuid::Empty; uint8 slot = fields[46].GetUInt8(); @@ -27663,6 +27673,15 @@ void Player::SendPlayerChoice(ObjectGuid sender, int32 choiceId) SendDirectMessage(displayPlayerChoice.Write()); } +bool Player::MeetPlayerCondition(uint32 conditionId) const +{ + if (PlayerConditionEntry const* playerCondition = sPlayerConditionStore.LookupEntry(conditionId)) + if (!ConditionMgr::IsPlayerMeetingCondition(this, playerCondition)) + return false; + + return true; +} + float Player::GetCollisionHeight(bool mounted) const { if (mounted) diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index e0022324bc6..93dcfb4b593 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -2378,6 +2378,8 @@ class TC_GAME_API Player : public Unit, public GridObject<Player> void SendPlayerChoice(ObjectGuid sender, int32 choiceId); + bool MeetPlayerCondition(uint32 conditionId) const; + protected: // Gamemaster whisper whitelist GuidList WhisperList; diff --git a/src/server/game/Handlers/ArtifactHandler.cpp b/src/server/game/Handlers/ArtifactHandler.cpp index 774dedff867..8e9451daaf9 100644 --- a/src/server/game/Handlers/ArtifactHandler.cpp +++ b/src/server/game/Handlers/ArtifactHandler.cpp @@ -35,9 +35,11 @@ void WorldSession::HandleArtifactAddPower(WorldPackets::Artifact::ArtifactAddPow if (!artifact) return; + uint32 currentArtifactTier = artifact->GetModifier(ITEM_MODIFIER_ARTIFACT_TIER); + uint64 xpCost = 0; if (GtArtifactLevelXPEntry const* cost = sArtifactLevelXPGameTable.GetRow(artifact->GetTotalPurchasedArtifactPowers() + 1)) - xpCost = uint64(artifact->GetModifier(ITEM_MODIFIER_ARTIFACT_TIER) == 1 ? cost->XP2 : cost->XP); + xpCost = uint64(currentArtifactTier == MAX_ARTIFACT_TIER ? cost->XP2 : cost->XP); if (xpCost > artifact->GetUInt64Value(ITEM_FIELD_ARTIFACT_XP)) return; @@ -53,32 +55,47 @@ void WorldSession::HandleArtifactAddPower(WorldPackets::Artifact::ArtifactAddPow if (!artifactPowerEntry) return; - if (artifactAddPower.PowerChoices[0].Rank != artifactPower->PurchasedRank + 1 || - artifactAddPower.PowerChoices[0].Rank > artifactPowerEntry->MaxPurchasableRank) + if (artifactPowerEntry->Tier > currentArtifactTier) return; - if (std::unordered_set<uint32> const* artifactPowerLinks = sDB2Manager.GetArtifactPowerLinks(artifactPower->ArtifactPowerId)) + uint32 maxRank = artifactPowerEntry->MaxPurchasableRank; + if (artifactPowerEntry->Tier < currentArtifactTier) { - bool hasAnyLink = false; - for (uint32 artifactPowerLinkId : *artifactPowerLinks) - { - ArtifactPowerEntry const* artifactPowerLink = sArtifactPowerStore.LookupEntry(artifactPowerLinkId); - if (!artifactPowerLink) - continue; + if (artifactPowerEntry->Flags & ARTIFACT_POWER_FLAG_FINAL) + maxRank = 1; + else if (artifactPowerEntry->Flags & ARTIFACT_POWER_FLAG_MAX_RANK_WITH_TIER) + maxRank += currentArtifactTier - artifactPowerEntry->Tier; + } - ItemDynamicFieldArtifactPowers const* artifactPowerLinkLearned = artifact->GetArtifactPower(artifactPowerLinkId); - if (!artifactPowerLinkLearned) - continue; + if (artifactAddPower.PowerChoices[0].Rank != artifactPower->PurchasedRank + 1 || + artifactAddPower.PowerChoices[0].Rank > maxRank) + return; - if (artifactPowerLinkLearned->PurchasedRank >= artifactPowerLink->MaxPurchasableRank) + if (!(artifactPowerEntry->Flags & ARTIFACT_POWER_FLAG_NO_LINK_REQUIRED)) + { + if (std::unordered_set<uint32> const* artifactPowerLinks = sDB2Manager.GetArtifactPowerLinks(artifactPower->ArtifactPowerId)) + { + bool hasAnyLink = false; + for (uint32 artifactPowerLinkId : *artifactPowerLinks) { - hasAnyLink = true; - break; + ArtifactPowerEntry const* artifactPowerLink = sArtifactPowerStore.LookupEntry(artifactPowerLinkId); + if (!artifactPowerLink) + continue; + + ItemDynamicFieldArtifactPowers const* artifactPowerLinkLearned = artifact->GetArtifactPower(artifactPowerLinkId); + if (!artifactPowerLinkLearned) + continue; + + if (artifactPowerLinkLearned->PurchasedRank >= artifactPowerLink->MaxPurchasableRank) + { + hasAnyLink = true; + break; + } } - } - if (!hasAnyLink) - return; + if (!hasAnyLink) + return; + } } ArtifactPowerRankEntry const* artifactPowerRank = sDB2Manager.GetArtifactPowerRank(artifactPower->ArtifactPowerId, artifactPower->CurrentRankWithBonus + 1 - 1); // need data for next rank, but -1 because of how db2 data is structured @@ -115,6 +132,31 @@ void WorldSession::HandleArtifactAddPower(WorldPackets::Artifact::ArtifactAddPow artifact->SetUInt64Value(ITEM_FIELD_ARTIFACT_XP, artifact->GetUInt64Value(ITEM_FIELD_ARTIFACT_XP) - xpCost); artifact->SetState(ITEM_CHANGED, _player); + + uint32 totalPurchasedArtifactPower = artifact->GetTotalPurchasedArtifactPowers(); + uint32 artifactTier = 0; + + for (ArtifactTierEntry const* tier : sArtifactTierStore) + { + if (artifactPowerEntry->Flags & ARTIFACT_POWER_FLAG_FINAL && artifactPowerEntry->Tier < MAX_ARTIFACT_TIER) + { + artifactTier = artifactPowerEntry->Tier + 1; + break; + } + + if (totalPurchasedArtifactPower < tier->MaxNumTraits) + { + artifactTier = tier->ArtifactTier; + break; + } + } + + artifactTier = std::max(artifactTier, currentArtifactTier); + + for (uint32 i = currentArtifactTier; i <= artifactTier; ++i) + artifact->InitArtifactPowers(artifact->GetTemplate()->GetArtifactID(), uint8(i)); + + artifact->SetModifier(ITEM_MODIFIER_ARTIFACT_TIER, artifactTier); } void WorldSession::HandleArtifactSetAppearance(WorldPackets::Artifact::ArtifactSetAppearance& artifactSetAppearance) diff --git a/src/server/game/Handlers/CharacterHandler.cpp b/src/server/game/Handlers/CharacterHandler.cpp index a428843d6be..3ba93420747 100644 --- a/src/server/game/Handlers/CharacterHandler.cpp +++ b/src/server/game/Handlers/CharacterHandler.cpp @@ -20,6 +20,7 @@ #include "AccountMgr.h" #include "ArenaTeam.h" #include "ArenaTeamMgr.h" +#include "ArtifactPackets.h" #include "AuthenticationPackets.h" #include "Battleground.h" #include "BattlegroundPackets.h" @@ -971,6 +972,16 @@ void WorldSession::HandlePlayerLogin(LoginQueryHolder* holder) WorldPackets::BattlePet::BattlePetJournalLockAcquired lock; SendPacket(lock.Write()); + WorldPackets::Artifact::ArtifactKnowledge artifactKnowledge; + artifactKnowledge.ArtifactCategoryID = ARTIFACT_CATEGORY_PRIMARY; + artifactKnowledge.KnowledgeLevel = sWorld->getIntConfig(CONFIG_CURRENCY_START_ARTIFACT_KNOWLEDGE); + SendPacket(artifactKnowledge.Write()); + + WorldPackets::Artifact::ArtifactKnowledge artifactKnowledgeFishingPole; + artifactKnowledgeFishingPole.ArtifactCategoryID = ARTIFACT_CATEGORY_FISHING; + artifactKnowledgeFishingPole.KnowledgeLevel = 0; + SendPacket(artifactKnowledgeFishingPole.Write()); + pCurrChar->SendInitialPacketsBeforeAddToMap(); //Show cinematic at the first time that player login diff --git a/src/server/game/Server/Packets/ArtifactPackets.cpp b/src/server/game/Server/Packets/ArtifactPackets.cpp index 2612b384f8c..8666b709cf0 100644 --- a/src/server/game/Server/Packets/ArtifactPackets.cpp +++ b/src/server/game/Server/Packets/ArtifactPackets.cpp @@ -69,3 +69,11 @@ WorldPacket const* WorldPackets::Artifact::ArtifactXpGain::Write() return &_worldPacket; } + +WorldPacket const* WorldPackets::Artifact::ArtifactKnowledge::Write() +{ + _worldPacket << int32(ArtifactCategoryID); + _worldPacket << int8(KnowledgeLevel); + + return &_worldPacket; +} diff --git a/src/server/game/Server/Packets/ArtifactPackets.h b/src/server/game/Server/Packets/ArtifactPackets.h index f208a906413..25934b75c28 100644 --- a/src/server/game/Server/Packets/ArtifactPackets.h +++ b/src/server/game/Server/Packets/ArtifactPackets.h @@ -99,6 +99,17 @@ namespace WorldPackets ObjectGuid ArtifactGUID; uint64 Amount = 0; }; + + class ArtifactKnowledge final : public ServerPacket + { + public: + ArtifactKnowledge() : ServerPacket(SMSG_ARTIFACT_KNOWLEDGE, 1 + 4) { } + + WorldPacket const* Write() override; + + int32 ArtifactCategoryID = 0; + int8 KnowledgeLevel = 0; + }; } } diff --git a/src/server/game/Server/Protocol/Opcodes.cpp b/src/server/game/Server/Protocol/Opcodes.cpp index aabd733d554..77690ba7354 100644 --- a/src/server/game/Server/Protocol/Opcodes.cpp +++ b/src/server/game/Server/Protocol/Opcodes.cpp @@ -894,7 +894,7 @@ void OpcodeTable::Initialize() DEFINE_SERVER_OPCODE_HANDLER(SMSG_ARENA_ERROR, STATUS_UNHANDLED, CONNECTION_TYPE_REALM); DEFINE_SERVER_OPCODE_HANDLER(SMSG_ARENA_PREP_OPPONENT_SPECIALIZATIONS, STATUS_UNHANDLED, CONNECTION_TYPE_REALM); DEFINE_SERVER_OPCODE_HANDLER(SMSG_ARTIFACT_FORGE_OPENED, STATUS_NEVER, CONNECTION_TYPE_REALM); - DEFINE_SERVER_OPCODE_HANDLER(SMSG_ARTIFACT_KNOWLEDGE, STATUS_UNHANDLED, CONNECTION_TYPE_REALM); + DEFINE_SERVER_OPCODE_HANDLER(SMSG_ARTIFACT_KNOWLEDGE, STATUS_NEVER, CONNECTION_TYPE_REALM); DEFINE_SERVER_OPCODE_HANDLER(SMSG_ARTIFACT_RESPEC_CONFIRM, STATUS_UNHANDLED, CONNECTION_TYPE_REALM); DEFINE_SERVER_OPCODE_HANDLER(SMSG_ARTIFACT_TRAITS_REFUNDED, STATUS_UNHANDLED, CONNECTION_TYPE_REALM); DEFINE_SERVER_OPCODE_HANDLER(SMSG_ARTIFACT_XP_GAIN, STATUS_NEVER, CONNECTION_TYPE_REALM); diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index f90fc39c36d..23a62783c83 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -956,6 +956,13 @@ void World::LoadConfigSettings(bool reload) } m_int_configs[CONFIG_CURRENCY_MAX_JUSTICE_POINTS] *= 100; //precision mod + m_int_configs[CONFIG_CURRENCY_START_ARTIFACT_KNOWLEDGE] = sConfigMgr->GetIntDefault("Currency.StartArtifactKnowledge", 55); + if (int32(m_int_configs[CONFIG_CURRENCY_START_ARTIFACT_KNOWLEDGE]) < 0) + { + TC_LOG_ERROR("server.loading", "Currency.StartArtifactKnowledge (%i) must be >= 0, set to default 0.", m_int_configs[CONFIG_CURRENCY_START_ARTIFACT_KNOWLEDGE]); + m_int_configs[CONFIG_CURRENCY_START_ARTIFACT_KNOWLEDGE] = 0; + } + m_int_configs[CONFIG_MAX_RECRUIT_A_FRIEND_BONUS_PLAYER_LEVEL] = sConfigMgr->GetIntDefault("RecruitAFriend.MaxLevel", 85); if (m_int_configs[CONFIG_MAX_RECRUIT_A_FRIEND_BONUS_PLAYER_LEVEL] > m_int_configs[CONFIG_MAX_PLAYER_LEVEL]) { diff --git a/src/server/game/World/World.h b/src/server/game/World/World.h index 7b0ad97cd8c..16850da37ae 100644 --- a/src/server/game/World/World.h +++ b/src/server/game/World/World.h @@ -253,6 +253,7 @@ enum WorldIntConfigs CONFIG_CURRENCY_START_APEXIS_CRYSTALS, CONFIG_CURRENCY_MAX_APEXIS_CRYSTALS, CONFIG_CURRENCY_START_JUSTICE_POINTS, + CONFIG_CURRENCY_START_ARTIFACT_KNOWLEDGE, CONFIG_CURRENCY_MAX_JUSTICE_POINTS, CONFIG_CURRENCY_RESET_HOUR, CONFIG_CURRENCY_RESET_DAY, diff --git a/src/server/worldserver/worldserver.conf.dist b/src/server/worldserver/worldserver.conf.dist index 1a514e87d05..6f187fca25d 100644 --- a/src/server/worldserver/worldserver.conf.dist +++ b/src/server/worldserver/worldserver.conf.dist @@ -3947,6 +3947,14 @@ Currency.StartJusticePoints = 0 Currency.MaxJusticePoints = 4000 # +# Currency.StartArtifactKnowledge +# Amount artifact knowledge that new players will start with +# Default: 55 (max) +# + +Currency.StartArtifactKnowledge = 55 + +# ################################################################################################### ################################################################################################### |