diff --git a/sql/updates/update_2023-05-18_2.sql b/sql/updates/update_2023-05-18_2.sql new file mode 100644 index 000000000..8d10c5ee4 --- /dev/null +++ b/sql/updates/update_2023-05-18_2.sql @@ -0,0 +1,26 @@ +CREATE TABLE IF NOT EXISTS `companions` ( + `companionId` bigint(20) NOT NULL AUTO_INCREMENT, + `accountId` bigint(20) NOT NULL, + `characterId` bigint(20) NULL DEFAULT NULL, + `monsterId` int(11) NOT NULL, + `name` varchar(64) NOT NULL, + `slot` tinyint(1) NOT NULL, + `barrackLayer` tinyint(1) NOT NULL DEFAULT '1', + `bx` float NOT NULL, + `by` float NOT NULL, + `bz` float NOT NULL, + `dx` float NOT NULL, + `dy` float NOT NULL, + `exp` int(11) NOT NULL DEFAULT '0', + `stamina` int(11) NOT NULL DEFAULT '60000', + `adoptTime` DATETIME DEFAULT CURRENT_TIMESTAMP, + `active` tinyint(1) DEFAULT '1', + PRIMARY KEY (`companionId`), + KEY `accountId` (`accountId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ; + +ALTER TABLE `companions` + ADD CONSTRAINT `companions_ibfk_1` FOREIGN KEY (`accountId`) REFERENCES `accounts` (`accountId`) ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE `companions` + ADD CONSTRAINT `companions_ibfk_2` FOREIGN KEY (`characterId`) REFERENCES `characters` (`characterId`) ON DELETE SET NULL ON UPDATE CASCADE; \ No newline at end of file diff --git a/src/BarracksServer/Database/Account.cs b/src/BarracksServer/Database/Account.cs index 29fbf68e3..7d889abf8 100644 --- a/src/BarracksServer/Database/Account.cs +++ b/src/BarracksServer/Database/Account.cs @@ -14,6 +14,7 @@ public class Account : IAccount { private readonly object _moneyLock = new(); private readonly List _characters = new(); + private readonly List _companions = new List(); /// /// Gets or sets account's id. @@ -211,6 +212,10 @@ public static Account LoadFromDb(string accountName) foreach (var character in characters) account.AddCharacter(character); + var companions = BarracksServer.Instance.Database.GetCompanions(account.Id); + foreach (var companion in companions) + account.AddCompanion(companion); + BarracksServer.Instance.Database.LoadMailbox(account); return account; @@ -330,5 +335,47 @@ public void Save() BarracksServer.Instance.Database.SaveCharacter(character); } } + + /// + /// Returns companion by companion id, or null if it doesn't exist. + /// + /// + /// + public Companion GetCompanionById(long id) + { + lock (_companions) + return _companions.FirstOrDefault(a => a.ObjectId == id); + } + + /// + /// Returns list of all companions on account. + /// + /// + public Companion[] GetCompanions() + { + lock (_companions) + return _companions.ToArray(); + } + + /// + /// Adds companion to account object and assigns index. + /// + /// + private void AddCompanion(Companion companion) + { + lock (_companions) + { + for (byte i = 1; i <= byte.MaxValue; ++i) + { + if (!_companions.Any(a => a.Index == i)) + { + companion.Index = i; + break; + } + } + + _companions.Add(companion); + } + } } } diff --git a/src/BarracksServer/Database/BarracksDb.cs b/src/BarracksServer/Database/BarracksDb.cs index a3ee2ce59..d78454d27 100644 --- a/src/BarracksServer/Database/BarracksDb.cs +++ b/src/BarracksServer/Database/BarracksDb.cs @@ -3,8 +3,10 @@ using System.IO; using System.Linq; using System.Text; +using Melia.Barracks.Network; using Melia.Shared.Database; using Melia.Shared.Game.Const; +using Melia.Shared.Network; using Melia.Shared.World; using MySqlConnector; using Yggdrasil.Logging; @@ -571,5 +573,85 @@ public void SaveItem(Character character, long itemId) trans.Commit(); } } + + /// + /// Returns all companions on given account. + /// + /// + /// + public List GetCompanions(long accountId) + { + var result = new List(); + + using (var conn = this.GetConnection()) + { + using (var mc = new MySqlCommand("SELECT * FROM `companions` WHERE `accountId` = @accountId ORDER BY `slot`", conn)) + { + mc.Parameters.AddWithValue("@accountId", accountId); + + using (var reader = mc.ExecuteReader()) + { + while (reader.Read()) + { + var characterId = reader.IsDBNull(2) ? 0 : reader.GetInt64("characterId"); + var companion = new Companion(reader.GetInt64("companionId"), reader.GetInt64("accountId"), characterId); + companion.MonsterId = reader.GetInt32("monsterId"); + companion.Name = reader.GetStringSafe("name"); + companion.Index = (byte)reader.GetInt32("slot"); + companion.BarracksLayer = reader.GetInt32("barrackLayer"); + + var bx = reader.GetFloat("bx"); + var by = reader.GetFloat("by"); + var bz = reader.GetFloat("bz"); + companion.BarracksPosition = new Position(bx, by, bz); + + result.Add(companion); + } + } + } + } + + return result; + } + + /// + /// Set the current character associated with a companion + /// + /// + /// + public void SetCompanionCharacter(long companionId, long characterId) + { + using (var conn = this.GetConnection()) + using (var trans = conn.BeginTransaction()) + { + using (var cmd = new UpdateCommand("UPDATE `companions` SET {0} WHERE `companionId` = @companionId", conn, trans)) + { + cmd.AddParameter("@companionId", companionId); + if (characterId > 0) + cmd.Set("characterId", characterId); + else + cmd.Set("characterId", null); + + cmd.Execute(); + } + trans.Commit(); + } + } + + /// + /// Deletes a companion. + /// + /// + /// + public bool DeleteCompanion(long companionId) + { + using (var conn = this.GetConnection()) + using (var mc = new MySqlCommand("DELETE FROM `companions` WHERE `companionId` = @companionId", conn)) + { + mc.Parameters.AddWithValue("@companionId", companionId); + + return mc.ExecuteNonQuery() > 0; + } + } } } diff --git a/src/BarracksServer/Database/Companion.cs b/src/BarracksServer/Database/Companion.cs new file mode 100644 index 000000000..a6e28358a --- /dev/null +++ b/src/BarracksServer/Database/Companion.cs @@ -0,0 +1,85 @@ +using Melia.Shared.ObjectProperties; +using Melia.Shared.Game.Const; +using Melia.Shared.World; + +namespace Melia.Barracks.Database +{ + public class Companion + { + /// + /// Companion's unique id. + /// + public long DbId { get; set; } + + /// + /// Companion's globally unique id. + /// + public long ObjectId => ObjectIdRanges.Companions + this.DbId; + + /// + /// Id of the companion's account. + /// + public long AccountDbId { get; set; } + + /// + /// Id of the associated character. + /// + public long CharacterDbId { get; set; } = 0; + + /// + /// Globally unique character id. + /// + public long CharacterObjectId => (this.CharacterDbId != 0) ? ObjectIdRanges.Characters + this.CharacterDbId : 0; + + /// + /// Visual Id of the companion + /// + public int MonsterId { get; set; } + + /// + /// Index of companion in companion list. + /// + public byte Index { get; set; } + + /// + /// Companion's name. + /// + public string Name { get; set; } + + /// + /// Layer in the barrack that the companion should appear in. + /// + public int BarracksLayer { get; set; } + + /// + /// Companion's position in barracks. + /// + public Position BarracksPosition { get; set; } + + /// + /// Companion's direction in barracks. + /// + public Direction BarracksDirection { get; set; } + + /// + /// Companion's experience point shown as level in barracks. + /// + public long Exp { get; set; } + + /// + /// Companion ctor + /// + /// + /// + /// Set to 0 means no unassigned companion + public Companion(long id, long accountId, long characterId = 0) + { + this.DbId = id; + this.AccountDbId = accountId; + this.CharacterDbId = characterId; + this.BarracksPosition = new Position(30.350805f, 17.948700f, 7.398606f); + this.BarracksDirection = new Direction(45); + this.BarracksLayer = 1; + } + } +} diff --git a/src/BarracksServer/Network/Helpers/BarrackPetHelper.cs b/src/BarracksServer/Network/Helpers/BarrackPetHelper.cs new file mode 100644 index 000000000..7c09bb103 --- /dev/null +++ b/src/BarracksServer/Network/Helpers/BarrackPetHelper.cs @@ -0,0 +1,49 @@ +using System; +using Melia.Barracks.Database; +using Melia.Shared.Network; + +namespace Melia.Barracks.Network.Helpers +{ + /// + /// Contains extensions for writing barracks character data to packets. + /// + public static class BarrackPetHelper + { + /// + /// Writes the character's data to the packet. + /// + /// + /// + /// + public static void AddCompanion(this Packet packet, Companion companion) + { + packet.PutInt(companion.MonsterId); + packet.PutLong(companion.ObjectId); + packet.PutLong(companion.CharacterObjectId); + packet.PutLong(companion.Exp); + packet.PutLpString(companion.Name); + packet.PutByte(0); + packet.PutFloat(companion.BarracksPosition.X); + packet.PutFloat(companion.BarracksPosition.Y); + packet.PutFloat(companion.BarracksPosition.Z); + packet.PutFloat(companion.BarracksDirection.Cos); + packet.PutFloat(companion.BarracksDirection.Sin); + packet.PutLong(0); + packet.PutByte(companion.Index); + var weaponSlots = (byte)0; + packet.PutInt(weaponSlots); + for (var i = 0; i < weaponSlots; i++) + packet.PutByte(0); // Is Equipped + packet.PutByte(1); + var armorSlots = (byte)0; + packet.PutShort(armorSlots); + packet.PutShort(0); // Property Count + for (var i = 0; i < armorSlots; i++) + packet.PutByte(0); + packet.PutByte(1); + packet.PutInt(1); + packet.PutByte(0); + packet.PutLong(60000); + } + } +} diff --git a/src/BarracksServer/Network/PacketHandler.cs b/src/BarracksServer/Network/PacketHandler.cs index c9b9a6f19..924ccacbd 100644 --- a/src/BarracksServer/Network/PacketHandler.cs +++ b/src/BarracksServer/Network/PacketHandler.cs @@ -161,6 +161,7 @@ public void CB_START_BARRACK(IBarracksConnection conn, Packet packet) //Send.BC_NORMAL.SetBarrack(conn, conn.Account.SelectedBarrack); Send.BC_COMMANDER_LIST(conn); Send.BC_NORMAL.CharacterInfo(conn); + Send.BC_NORMAL.CompanionInfo(conn); Send.BC_NORMAL.TeamUI(conn); Send.BC_NORMAL.ZoneTraffic(conn); @@ -663,6 +664,7 @@ public void CB_SELECT_BARRACK_LAYER(IBarracksConnection conn, Packet packet) Send.BC_NORMAL.SetBarrack(conn, conn.Account.SelectedBarrack); Send.BC_COMMANDER_LIST(conn); Send.BC_NORMAL.CharacterInfo(conn); + Send.BC_NORMAL.CompanionInfo(conn); Send.BC_NORMAL.TeamUI(conn); } @@ -677,7 +679,18 @@ public void CB_PET_PC(IBarracksConnection conn, Packet packet) var companionId = packet.GetLong(); var characterId = packet.GetLong(); - // ... + var companion = conn.Account.GetCompanionById(companionId); + var character = conn.Account.GetCharacterById(characterId); + + if (companion == null) + { + Log.Warning("CB_PET_PC: Companion not found by id '{0}' received from '{1}'.", companionId, conn.Account.Name); + return; + } + + companion.CharacterDbId = character?.DbId ?? 0; + BarracksServer.Instance.Database.SetCompanionCharacter(companion.DbId, companion.CharacterDbId); + Send.BC_NORMAL.SetCompanion(conn, companion.ObjectId, character?.ObjectId ?? 0); } /// @@ -692,7 +705,25 @@ public void CB_PET_COMMAND(IBarracksConnection conn, Packet packet) var characterId = packet.GetLong(); var command = packet.GetByte(); // 0 : revive request; 1 : delete pet request. - // ... + var companion = conn.Account.GetCompanionById(companionId); + if (companion == null) + { + Log.Warning("CB_PET_COMMAND: Companion not found by id '{0}' received from '{1}'.", companionId, conn.Account.Name); + return; + } + + companion.CharacterDbId = characterId; + switch (command) + { + // Delete + case 1: + if (BarracksServer.Instance.Database.DeleteCompanion(companion.DbId)) + { + Send.BC_NORMAL.DeleteCompanion(conn, companionId); + Send.BC_NORMAL.TeamUI(conn); + } + break; + } } /// @@ -976,5 +1007,26 @@ public void CB_CHARACTER_SWAP_SLOT(IBarracksConnection conn, Packet packet) Send.BC_CHARACTER_SLOT_SWAP_SUCCESS(conn); Send.BC_COMMANDER_LIST(conn); } + + /// + /// Sent when a companion requests to move in barracks + /// + /// + /// + [PacketHandler(Op.CB_COMPANION_MOVE)] + public void CB_COMPANION_MOVE(IBarracksConnection conn, Packet packet) + { + var companionId = packet.GetLong(); + var position = packet.GetPosition(); + var direction = packet.GetDirection(); + + var companion = conn.Account.GetCompanionById(companionId); + if (companion != null) + { + companion.BarracksPosition = position; + companion.BarracksDirection = direction; + Send.BC_NORMAL.SetCompanionPosition(conn, companionId, position); + } + } } } diff --git a/src/BarracksServer/Network/Send.Normal.cs b/src/BarracksServer/Network/Send.Normal.cs index 5590bfe85..7c2d058bf 100644 --- a/src/BarracksServer/Network/Send.Normal.cs +++ b/src/BarracksServer/Network/Send.Normal.cs @@ -372,6 +372,85 @@ public static void StartGameFailed(IBarracksConnection conn) conn.Send(packet); } + + /// + /// Moves a companion in the barrack. + /// + /// + /// + /// + public static void SetCompanionPosition(IBarracksConnection conn, long companionId, Position position) + { + var packet = new Packet(Op.BC_NORMAL); + packet.PutInt(NormalOp.Barrack.SetCompanionPosition); + + packet.PutLong(conn.Account.Id); + packet.PutLong(companionId); + packet.PutFloat(position.X); + packet.PutFloat(position.Y); + packet.PutFloat(position.Z); + + conn.Send(packet); + } + + /// + /// Adds companions in the barrack. + /// + /// + public static void CompanionInfo(IBarracksConnection conn) + { + var allCompanions = conn.Account.GetCompanions(); + var layerCompanions = allCompanions.Where(x => x.BarracksLayer == conn.Account.SelectedBarrackLayer); + var companionCount = layerCompanions.Count(); + if (companionCount == 0) + return; + var packet = new Packet(Op.BC_NORMAL); + packet.PutInt(NormalOp.Barrack.CompanionInfo); + + packet.PutLong(conn.Account.Id); + packet.PutInt(companionCount); + foreach (var companion in layerCompanions) + { + packet.AddCompanion(companion); + } + packet.PutInt(companionCount); + + conn.Send(packet); + } + + /// + /// Set a companion's associated character in the barrack. + /// + /// + /// + /// + public static void SetCompanion(IBarracksConnection conn, long companionId, long characterId) + { + var packet = new Packet(Op.BC_NORMAL); + packet.PutInt(NormalOp.Barrack.SetCompanion); + + packet.PutLong(conn.Account.Id); + packet.PutLong(companionId); + packet.PutLong(characterId); + + conn.Send(packet); + } + + /// + /// Deletes a companion in the barrack. + /// + /// + /// + public static void DeleteCompanion(IBarracksConnection conn, long companionId) + { + var packet = new Packet(Op.BC_NORMAL); + packet.PutInt(NormalOp.Barrack.DeleteCompanion); + + packet.PutLong(conn.Account.Id); + packet.PutLong(companionId); + + conn.Send(packet); + } } } } diff --git a/src/Shared/Data/Database/Companions.cs b/src/Shared/Data/Database/Companions.cs new file mode 100644 index 000000000..46b25fc24 --- /dev/null +++ b/src/Shared/Data/Database/Companions.cs @@ -0,0 +1,59 @@ +using System.Linq; +using Melia.Shared.Game.Const; +using Newtonsoft.Json.Linq; +using Yggdrasil.Data.JSON; + +namespace Melia.Shared.Data.Database +{ + public class CompanionData + { + public int Id { get; set; } + public string ClassName { get; set; } + public string Name { get; set; } + public float RideMSPD { get; set; } + public bool IsPremium { get; set; } + public bool CanPet { get; set; } + public bool CanRide { get; set; } + public bool EndRideOnHit { get; set; } + public int FoodGroup { get; set; } + public BuffId Buff { get; internal set; } + public int Price { get; set; } + public string FeedAnimation { get; set; } + public int FeedSleep { get; set; } + public bool IsRideOnly { get; set; } + } + public class CompanionDb : DatabaseJsonIndexed + { + public bool TryFindByClassName(string className, out CompanionData data) + { + data = this.Entries.Values.FirstOrDefault(a => a.ClassName == className); + return data != null; + } + + protected override void ReadEntry(JObject entry) + { + // Values: id, className, name, rideMSPD, premium, pet, ride, endRideOnHit, [foodGroup], [buff] + entry.AssertNotMissing("id", "className"); + + var data = new CompanionData(); + + data.Id = entry.ReadInt("id"); + data.ClassName = entry.ReadString("className"); + data.Name = entry.ReadString("name"); + if (entry.ContainsKey("price")) + data.Price = entry.ReadInt("price"); + data.RideMSPD = entry.ReadFloat("rideMSPD"); + data.IsPremium = entry.ReadBool("premium"); + data.CanPet = entry.ReadBool("pet"); + data.CanRide = entry.ReadBool("ride"); + data.EndRideOnHit = entry.ReadBool("endRideOnHit"); + data.FoodGroup = entry.ReadInt("foodGroup", -1); + if (entry.ContainsKey("buff") && !string.IsNullOrEmpty(data.Name)) + data.Buff = entry.ReadEnum("buff"); + data.FeedAnimation = entry.ReadInt("feedAnimation").ToString(); + data.FeedSleep = entry.ReadInt("feedSleep"); + + this.Entries[data.Id] = data; + } + } +} diff --git a/src/Shared/Data/Database/Monsters.cs b/src/Shared/Data/Database/Monsters.cs index a73bd5493..bc193d03e 100644 --- a/src/Shared/Data/Database/Monsters.cs +++ b/src/Shared/Data/Database/Monsters.cs @@ -30,6 +30,12 @@ public class MonsterData public int Exp { get; set; } public int JobExp { get; set; } + public int STR { get; set; } + public int DEX { get; set; } + public int CON { get; set; } + public int INT { get; set; } + public int MNA { get; set; } + public float Hp { get; set; } public float Sp { get; set; } public float PhysicalAttackMin { get; set; } diff --git a/src/Shared/Data/MeliaData.cs b/src/Shared/Data/MeliaData.cs index 993e81343..ed1e7c36b 100644 --- a/src/Shared/Data/MeliaData.cs +++ b/src/Shared/Data/MeliaData.cs @@ -16,6 +16,7 @@ public class MeliaData public BuffDb BuffDb = new BuffDb(); public ChatMacroDb ChatMacroDb = new ChatMacroDb(); public CollectionDb CollectionDb; + public CompanionDb CompanionDb { get; } = new CompanionDb(); public CooldownDb CooldownDb = new CooldownDb(); public CustomCommandDb CustomCommandDb = new CustomCommandDb(); public DialogDb DialogDb = new DialogDb(); diff --git a/src/Shared/Network/NormalOp.cs b/src/Shared/Network/NormalOp.cs index 597fda8d6..c753edbb0 100644 --- a/src/Shared/Network/NormalOp.cs +++ b/src/Shared/Network/NormalOp.cs @@ -11,7 +11,11 @@ public static class Barrack { public const int SetBarrackCharacter = 0x00; public const int SetPosition = 0x02; + public const int SetCompanionPosition = 0x03; public const int SetBarrack = 0x05; + public const int CompanionInfo = 0x09; + public const int SetCompanion = 0x0A; + public const int DeleteCompanion = 0x0B; public const int TeamUI = 0x0C; public const int ZoneTraffic = 0x0D; public const int StartGameFailed = 0x0F; @@ -59,8 +63,14 @@ public static class Zone public const int SetHitDelay = 0x78; public const int SkillCancelCancel = 0x7D; public const int SpinObject = 0x8A; + public const int PetPlayAnimation = 0x90; public const int OpenBook = 0x9E; public const int Unknown_A1 = 0xA1; + public const int PetInfo = 0xA4; + public const int Pet_AssociateHandleWorldId = 0xA9; + public const int PetExpUpdate = 0xAA; + public const int RidePet = 0xB5; + public const int PetOwner = 0xB6; public const int LeapJump = 0xC2; public const int Unknown_DA = 0xDA; public const int ItemCollectionList = 0xDD; @@ -81,6 +91,7 @@ public static class Zone public const int Unknown_19B = 0x19E; public const int PadSetModel = 0x1AB; public const int WigVisibilityUpdate = 0x1AC; + public const int PetIsInactive = 0x1A6; public const int Unknown_1B4 = 0x1B7; public const int ActorRotate = 0x1BF; public const int SubWeaponVisibilityUpdate = 0x1C5; diff --git a/src/Shared/ObjectProperties/PropertyObject.cs b/src/Shared/ObjectProperties/PropertyObject.cs index cea47864c..51592d5e7 100644 --- a/src/Shared/ObjectProperties/PropertyObject.cs +++ b/src/Shared/ObjectProperties/PropertyObject.cs @@ -31,6 +31,7 @@ public static class ObjectIdRanges public const long Abilities = 0x0700000000000000; public const long SessionObjects = 0x0A00000000000000; public const long Quests = 0x0B00000000000000; + public const long Companions = 0x0C00000000000000; // Old stuff for referecence: diff --git a/src/Shared/Server.cs b/src/Shared/Server.cs index f5b6768be..3c4854f1e 100644 --- a/src/Shared/Server.cs +++ b/src/Shared/Server.cs @@ -229,6 +229,7 @@ public void LoadData(ServerType serverType) this.LoadDb(this.Data.BarrackDb, "db/barracks.txt"); this.LoadDb(this.Data.BuffDb, "db/buffs.txt"); this.LoadDb(this.Data.ChatMacroDb, "db/chatmacros.txt"); + this.LoadDb(this.Data.CompanionDb, "db/companions.txt"); this.LoadDb(this.Data.CooldownDb, "db/cooldowns.txt"); this.LoadDb(this.Data.CustomCommandDb, "db/customcommands.txt"); this.LoadDb(this.Data.DialogDb, "db/dialogues.txt"); diff --git a/src/ZoneServer/Buffs/Handlers/Common/RidingCompanion.cs b/src/ZoneServer/Buffs/Handlers/Common/RidingCompanion.cs new file mode 100644 index 000000000..016aec194 --- /dev/null +++ b/src/ZoneServer/Buffs/Handlers/Common/RidingCompanion.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Melia.Shared.Game.Const; +using Melia.Zone.Buffs.Base; +using Melia.Zone.World.Actors.Characters; +using Melia.Zone.World.Actors.CombatEntities.Components; + +namespace Melia.Zone.Buffs.Handlers.Common +{ + /// + /// Handle for the Riding Companion Buff, which gives certain bonuses. + /// + [BuffHandler(BuffId.RidingCompanion)] + public class RidingCompanion : BuffHandler + { + public override void OnActivate(Buff buff, ActivationType activationType) + { + var target = (Character)buff.Target; + + // Generic values from Velheider, should be companion specific? + target.Properties.Modify(PropertyName.MSPD_BM, 4f); + target.Properties.Modify(PropertyName.DR_BM, 3f); + target.Properties.Modify(PropertyName.DEF_BM, 12f); + } + + public override void OnEnd(Buff buff) + { + var target = (Character)buff.Target; + + // Generic values from Velheider, should be companion specific? + target.Properties.Modify(PropertyName.MSPD_BM, -4f); + target.Properties.Modify(PropertyName.DR_BM, -3f); + target.Properties.Modify(PropertyName.DEF_BM, -12f); + } + } +} diff --git a/src/ZoneServer/Commands/ChatCommands.Handlers.cs b/src/ZoneServer/Commands/ChatCommands.Handlers.cs index 22fadaaa2..66b318207 100644 --- a/src/ZoneServer/Commands/ChatCommands.Handlers.cs +++ b/src/ZoneServer/Commands/ChatCommands.Handlers.cs @@ -21,6 +21,7 @@ using Yggdrasil.Network.Communication; using Yggdrasil.Util; using Yggdrasil.Util.Commands; +using Melia.Shared.Game.Properties; namespace Melia.Zone.Commands { @@ -43,6 +44,8 @@ public ChatCommands() this.Add("buyabilpoint", "", "", this.HandleBuyAbilPoint); this.Add("intewarpByToken", "", "", this.HandleTokenWarp); this.Add("mic", "", "", this.HandleMic); + this.Add("pethire", "", "", this.HandlePetHire); + this.Add("petstat", "", "", this.HandlePetStat); // Custom Client Commands this.Add("buyshop", "", "", this.HandleBuyShop); @@ -327,6 +330,118 @@ private CommandResult HandleJump(Character sender, Character target, string mess return CommandResult.Okay; } + /// + /// Official slash command to hire a pet + /// + /// /pethire 3 Pet + /// + /// + /// + /// + /// + /// + private CommandResult HandlePetHire(Character sender, Character target, string message, string command, Arguments args) + { + // Since this command is sent via UI interactions, we'll not + // use any automated command result messages, but we'll leave + // debug messages for now, in case of unexpected values. + if (args.Count < 2) + { + Log.Debug("HandlePetHire: Invalid call by user '{0}': {1}", sender.Username, command); + return CommandResult.Okay; + } + + if (sender.Companions.HasCompanions) + return CommandResult.Okay; + + if (!int.TryParse(args.Get(0), out var petShopId)) + return CommandResult.InvalidArgument; + + if (!ZoneServer.Instance.Data.CompanionDb.TryFind(petShopId, out var data)) + return CommandResult.InvalidArgument; + + if (!ZoneServer.Instance.Data.MonsterDb.TryFind(data.ClassName, out var monData)) + return CommandResult.InvalidArgument; + + var targetNpcName = "Companion Trader"; + var currentNpcName = sender.Connection.CurrentDialog?.Npc.Name; + + if (string.IsNullOrEmpty(currentNpcName) || !currentNpcName.Contains(targetNpcName)) + return CommandResult.InvalidArgument; + + //TODO: Decide if we sell pets that don't have a price defined. + if (data.Price == 0) + return CommandResult.Okay; + + if (sender.Inventory.CountItem(ItemId.Silver) < data.Price) + { + sender.SystemMessage("OwnerDontHaveSilver"); + return CommandResult.Okay; + } + + if (sender.Inventory.Remove(ItemId.Silver, data.Price) != InventoryResult.Success) + return CommandResult.Fail; + + var companion = new Companion(sender, monData.Id); + if (args.Count > 1) + companion.Name = args.Get(1); + + sender.Companions.CreateCompanion(companion); + + return CommandResult.Okay; + } + + /// + /// Official slash command to raise pet stats + /// + /// /petstat 528525790635969 MHP 1 + /// + /// + /// + /// + /// + /// + private CommandResult HandlePetStat(Character sender, Character target, string message, string command, Arguments args) + { + // Since this command is sent via UI interactions, we'll not + // use any automated command result messages, but we'll leave + // debug messages for now, in case of unexpected values. + if (args.Count < 2) + { + Log.Debug("HandlePetStat: Invalid call by user '{0}': {1}", sender.Username, command); + return CommandResult.Okay; + } + + if (!sender.Companions.HasCompanions) + return CommandResult.Okay; + + if (long.TryParse(args.Get(0), out var companionObjectId)) + { + var companion = sender.Companions.GetCompanion(companionObjectId); + var propertyName = "Monster_Stat_" + args.Get(1); + + if (companion != null && PropertyTable.Exists("Monster", propertyName) + && int.TryParse(args.Get(2), out var modifierValue)) + { + var baseCost = propertyName == PropertyName.Stat_DEF ? 600 : 300; + var totalCost = 0; + var currentValue = companion.Properties.GetFloat(propertyName) - 1; + + for (var i = currentValue; i < (currentValue + modifierValue); i++) + totalCost += (int)Math.Floor(baseCost * Math.Pow(1.08, i)); + + if (sender.Inventory.CountItem(ItemId.Silver) >= totalCost) + { + sender.Inventory.Remove(ItemId.Silver, totalCost); + companion.Properties.Modify(propertyName, modifierValue); + Send.ZC_OBJECT_PROPERTY(sender.Connection, companion); + } + } + } + + return CommandResult.Okay; + } + /// /// Warps target to the specified map. /// diff --git a/src/ZoneServer/Database/ZoneDb.cs b/src/ZoneServer/Database/ZoneDb.cs index 495937f34..56caaf7a1 100644 --- a/src/ZoneServer/Database/ZoneDb.cs +++ b/src/ZoneServer/Database/ZoneDb.cs @@ -15,6 +15,7 @@ using Melia.Zone.World.Actors.Characters; using Melia.Zone.World.Actors.Characters.Components; using Melia.Zone.World.Actors.CombatEntities.Components; +using Melia.Zone.World.Actors.Monsters; using Melia.Zone.World.Items; using Melia.Zone.World.Maps; using Melia.Zone.World.Quests; @@ -173,6 +174,7 @@ public Character GetCharacter(long accountId, long characterId) this.LoadProperties("character_properties", "characterId", character.DbId, character.Properties); this.LoadProperties("character_etc_properties", "characterId", character.DbId, character.Etc.Properties); this.LoadCollections(character); + this.LoadCompanions(character); // Initialize the properties to trigger calculated properties // and to set some properties in case the character is new and @@ -436,6 +438,7 @@ public void SaveCharacter(Character character) this.SaveBuffs(character); this.SaveCooldowns(character); this.SaveQuests(character); + this.SaveCompanions(character); } /// @@ -1443,5 +1446,100 @@ private void LoadCollections(Character character) character.Properties.InvalidateAll(); } } + + /// + /// Inserts companion in database. + /// + /// + /// + /// + /// + public void CreateCompanion(long accountId, long characterId, Companion companion) + { + using (var conn = this.GetConnection()) + using (var trans = conn.BeginTransaction()) + { + using (var cmd = new InsertCommand("INSERT INTO `companions` {0}", conn, trans)) + { + companion.AdoptTime = DateTime.Now; + + cmd.Set("accountId", accountId); + cmd.Set("characterId", characterId); + cmd.Set("name", companion.Name); + cmd.Set("monsterId", companion.Id); + cmd.Set("stamina", companion.Stamina); + cmd.Set("exp", companion.Exp); + cmd.Set("adoptTime", companion.AdoptTime); + cmd.Set("active", companion.IsActivated ? 1 : 0); + + cmd.Execute(); + companion.DbId = cmd.LastId; + } + + trans.Commit(); + } + } + + /// + /// Loads character's companion from the database. + /// + /// + private void LoadCompanions(Character character) + { + using (var conn = this.GetConnection()) + using (var mc = new MySqlCommand("SELECT * FROM `companions` WHERE `characterId` = @characterId", conn)) + { + mc.Parameters.AddWithValue("@characterId", character.DbId); + + using (var reader = mc.ExecuteReader()) + { + if (!reader.Read()) + return; + + var companion = new Companion(character, reader.GetInt32("monsterId")); + companion.DbId = reader.GetInt64("companionId"); + companion.Name = reader.GetString("name"); + companion.Stamina = reader.GetInt32("stamina"); + companion.Exp = reader.GetInt64("exp"); + companion.IsActivated = reader.GetByte("active") == 1; + + companion.Position = Position.Zero; + companion.Direction = new Direction(0); + + character.Companions.AddCompanion(companion, true); + } + } + } + + /// + /// Persists the character's companions to the database. + /// + /// + private void SaveCompanions(Character character) + { + if (!character.Companions.HasCompanions) + return; + using (var conn = this.GetConnection()) + using (var trans = conn.BeginTransaction()) + { + foreach (var companion in character.Companions.GetList()) + { + using (var cmd = new UpdateCommand("UPDATE `companions` SET {0} WHERE `companionId` = @companionId", conn, trans)) + { + cmd.AddParameter("@companionId", companion.Id); + cmd.Set("accountId", character.AccountId); + cmd.Set("characterId", character.DbId); + cmd.Set("name", companion.Name); + cmd.Set("stamina", companion.Stamina); + cmd.Set("exp", companion.Exp); + cmd.Set("active", companion.IsActivated ? 1 : 0); + + cmd.Execute(); + } + } + + trans.Commit(); + } + } } } diff --git a/src/ZoneServer/Network/Helpers/CompanionHelper.cs b/src/ZoneServer/Network/Helpers/CompanionHelper.cs new file mode 100644 index 000000000..bc39f2ce1 --- /dev/null +++ b/src/ZoneServer/Network/Helpers/CompanionHelper.cs @@ -0,0 +1,41 @@ +using Melia.Shared.Network; +using Melia.Shared.Network.Helpers; +using Melia.Zone.World.Actors.Monsters; + +namespace Melia.Zone.Network.Helpers +{ + public static class CompanionHelper + { + public static void AddCompanion(this Packet packet, Companion companion) + { + // Safety check + if (companion == null) + return; + var properties = companion.Properties.GetAll(); + var propertiesSize = properties.GetByteCount(); + + packet.PutInt(companion.Id); + packet.PutLong(companion.ObjectId); + packet.PutLong(companion.Owner.ObjectId); + packet.PutLong(companion.Exp); + packet.PutLpString(companion.Name); + packet.PutByte(0); + packet.PutPosition(companion.Position); + packet.PutDirection(companion.Direction); + packet.PutInt(companion.Handle); + packet.PutShort(propertiesSize); + packet.AddProperties(properties); + packet.PutShort(0); + packet.PutByte(1); + packet.PutByte(1); + packet.PutInt(0); + packet.PutInt(1); + packet.PutByte(0); + packet.PutByte(1); + packet.PutByte(1); + packet.PutInt(0); + packet.PutInt(companion.Stamina); + packet.PutInt(0); + } + } +} diff --git a/src/ZoneServer/Network/Helpers/MonsterHelper.cs b/src/ZoneServer/Network/Helpers/MonsterHelper.cs index d55a79a07..e549dc8c8 100644 --- a/src/ZoneServer/Network/Helpers/MonsterHelper.cs +++ b/src/ZoneServer/Network/Helpers/MonsterHelper.cs @@ -48,8 +48,8 @@ public static void AddMonster(this Packet packet, IMonsterBase monster) packet.PutInt(appearanceSize); packet.PutShort(propertiesSize); - packet.PutInt(0); - packet.PutInt(0); + packet.PutInt(monster.AssociatedHandle); + packet.PutInt(monster.OwnerHandle); packet.PutShort(0); packet.PutByte(0); diff --git a/src/ZoneServer/Network/PacketHandler.cs b/src/ZoneServer/Network/PacketHandler.cs index 1b511b981..b2240cdfe 100644 --- a/src/ZoneServer/Network/PacketHandler.cs +++ b/src/ZoneServer/Network/PacketHandler.cs @@ -12,6 +12,7 @@ using Melia.Zone.Events; using Melia.Zone.Network.Helpers; using Melia.Zone.Scripting; +using Melia.Zone.Scripting.AI; using Melia.Zone.Scripting.Dialogues; using Melia.Zone.Skills.Handlers.Base; using Melia.Zone.World; @@ -229,6 +230,18 @@ public void CZ_GAME_READY(IZoneConnection conn, Packet packet) // will display the restored cooldowns Send.ZC_COOLDOWN_LIST(character, character.Components.Get().GetAll()); + if (character.Companions.HasCompanions) + { + foreach (var companion in character.Companions.GetList()) + { + Send.ZC_NORMAL.Pet_AssociateHandleWorldId(character, companion); + Send.ZC_OBJECT_PROPERTY(conn, companion); + if (companion.IsActivated) + companion.SetCompanionState(companion.IsActivated); + } + Send.ZC_NORMAL.PetInfo(character); + } + character.OpenEyes(); ZoneServer.Instance.ServerEvents.OnPlayerReady(character); @@ -3034,5 +3047,78 @@ public void CZ_BUFF_REMOVE(IZoneConnection conn, Packet packet) character.StopBuff(buffId); } + + /// + /// Client request to summon a companion + /// + /// + /// + [PacketHandler(Op.CZ_SUMMON_PET)] + public void CZ_SUMMON_PET(IZoneConnection conn, Packet packet) + { + var companionId = packet.GetLong(); + var petId = packet.GetInt(); + var i1 = packet.GetInt(); + + if (companionId != 0) + { + var character = conn.SelectedCharacter; + var companion = character.Companions.GetCompanion(companionId); + + if (companion != null && companion.Id == petId) + companion.SetCompanionState(!companion.IsActivated); + } + } + + /// + /// Ride pet request + /// + [PacketHandler(Op.CZ_VEHICLE_RIDE)] + public void CZ_VEHICLE_RIDE(IZoneConnection conn, Packet packet) + { + var petHandle = packet.GetInt(); + var isRiding = packet.GetByte() == 1; + + var character = conn.SelectedCharacter; + var monster = character.Map.GetMonster(petHandle); + + if (monster is Companion companion && companion.Owner.ObjectId == character.ObjectId) + { + if (isRiding) + { + character.StartBuff(BuffId.RidingCompanion, 0, 0, TimeSpan.Zero, companion); + companion.StartBuff(BuffId.TakingOwner, 0, 0, TimeSpan.Zero, character); + + Send.ZC_OBJECT_PROPERTY(character, PropertyName.MSPD, PropertyName.MSPD_BM, + PropertyName.DR, PropertyName.DR_BM, PropertyName.MHP, PropertyName.MHP_RATE_BM, + PropertyName.DEF, PropertyName.DEF_BM); + Send.ZC_MOVE_SPEED(character); + } + else + { + character.StopBuff(BuffId.RidingCompanion); + companion.StopBuff(BuffId.TakingOwner); + + Send.ZC_OBJECT_PROPERTY(character, PropertyName.MSPD, PropertyName.MSPD_BM, + PropertyName.DR, PropertyName.DR_BM, PropertyName.MHP, PropertyName.MHP_RATE_BM, + PropertyName.DEF, PropertyName.DEF_BM); + Send.ZC_MOVE_SPEED(character); + } + // TODO: Pause AiScript while riding (No Equivalent in Melia currently). + // For now lets just remove the AiComponent + if (isRiding) + { + companion.Components.Remove(); + } + else + { + var ai = new AiComponent(companion, "BasicMonster"); + companion.Components.Add(ai); + } + companion.IsRiding = isRiding; + Send.ZC_NORMAL.PetIsInactive(conn, companion); + Send.ZC_NORMAL.RidePet(character, companion); + } + } } } diff --git a/src/ZoneServer/Network/Send.Normal.cs b/src/ZoneServer/Network/Send.Normal.cs index ebb09e5e9..26969c446 100644 --- a/src/ZoneServer/Network/Send.Normal.cs +++ b/src/ZoneServer/Network/Send.Normal.cs @@ -1357,6 +1357,125 @@ public static void OpenBook(Character character, string bookName) character.Connection.Send(packet); } + + /// + /// Pet play animation/state? + /// + /// + /// + public static void PetPlayAnimation(IZoneConnection conn, Companion companion, int animationId, int i1 = 1, byte b1 = 0) + { + var packet = new Packet(Op.ZC_NORMAL); + packet.PutInt(NormalOp.Zone.PetPlayAnimation); + + packet.PutInt(companion.Handle); + packet.PutInt(animationId); + packet.PutInt(i1); + packet.PutByte(b1); + + conn.Send(packet); + } + + /// + /// Sends associated pets for a character. + /// + /// + public static void PetInfo(Character character) + { + var companions = character.Companions.GetList(); + + var packet = new Packet(Op.ZC_NORMAL); + + packet.PutInt(NormalOp.Zone.PetInfo); + packet.PutInt(4); // 3 or 4 + packet.PutInt(companions.Count); + foreach (var companion in companions) + packet.AddCompanion(companion); + + character.Connection.Send(packet); + } + + /// + /// Pet Associate World Id and Handle + /// + /// + /// + public static void Pet_AssociateHandleWorldId(Character character, Companion companion) + { + var packet = new Packet(Op.ZC_NORMAL); + packet.PutInt(NormalOp.Zone.Pet_AssociateHandleWorldId); + packet.PutInt(companion.Handle); + packet.PutLong(companion.ObjectId); + packet.PutByte(1); + + character.Connection.Send(packet); + } + + /// + /// Pet Unknown? + /// + /// + /// + public static void PetExpUpdate(Character character, Companion companion) + { + var packet = new Packet(Op.ZC_NORMAL); + packet.PutInt(NormalOp.Zone.PetExpUpdate); + packet.PutLong(companion.ObjectId); + packet.PutLong(companion.Exp); + + character.Connection.Send(packet); + } + + /// + /// Ride Pet + /// + /// + /// + public static void RidePet(Character character, Companion companion) + { + var packet = new Packet(Op.ZC_NORMAL); + packet.PutInt(NormalOp.Zone.RidePet); + + packet.PutInt(character.Handle); + packet.PutInt(companion.Handle); + packet.PutByte(companion.IsRiding); + packet.PutByte(companion.IsRiding); + packet.PutLpString(companion.Data.ClassName); + + character.Map.Broadcast(packet); + } + + /// + /// Pet handle association for other players? + /// + /// + /// + public static void PetOwner(IZoneConnection conn, Companion companion) + { + var packet = new Packet(Op.ZC_NORMAL); + packet.PutInt(NormalOp.Zone.PetOwner); + + packet.PutInt(companion.Handle); + packet.PutInt(companion.OwnerHandle); + + conn.Send(packet); + } + + /// + /// Sent when Pet is activated or disabled + /// + /// + /// + public static void PetIsInactive(IZoneConnection conn, Companion companion) + { + var packet = new Packet(Op.ZC_NORMAL); + packet.PutInt(NormalOp.Zone.PetIsInactive); + + packet.PutInt(companion.Handle); + packet.PutInt(companion.IsActivated ? 0 : 1); // Inverse of active + + conn.Send(packet); + } } } } diff --git a/src/ZoneServer/Network/Send.cs b/src/ZoneServer/Network/Send.cs index 3e221014e..d0be17fce 100644 --- a/src/ZoneServer/Network/Send.cs +++ b/src/ZoneServer/Network/Send.cs @@ -1693,7 +1693,7 @@ public static void ZC_DIALOG_NUMBERRANGE(IZoneConnection conn, string msg, int m /// Removes actor from all clients on the map it's on. /// /// - public static void ZC_LEAVE(IActor actor) + public static void ZC_LEAVE(IActor actor, short leaveType = 1) { var packet = new Packet(Op.ZC_LEAVE); diff --git a/src/ZoneServer/Network/ZoneConnection.cs b/src/ZoneServer/Network/ZoneConnection.cs index a27a426eb..c4e4b94ce 100644 --- a/src/ZoneServer/Network/ZoneConnection.cs +++ b/src/ZoneServer/Network/ZoneConnection.cs @@ -85,6 +85,13 @@ protected override void OnClosed(ConnectionCloseType type) if (!justSaved) this.SaveAccountAndCharacter(); + if (character != null) + { + foreach (var companion in character?.Companions.GetList()) + if (companion.IsActivated) + character?.Map.RemoveMonster(companion); + } + character?.Map.RemoveCharacter(character); } diff --git a/src/ZoneServer/Scripting/ScriptableFunctions.cs b/src/ZoneServer/Scripting/ScriptableFunctions.cs index 3d700287a..25cb16c02 100644 --- a/src/ZoneServer/Scripting/ScriptableFunctions.cs +++ b/src/ZoneServer/Scripting/ScriptableFunctions.cs @@ -29,6 +29,7 @@ public static class ScriptableFunctions public static readonly DelegateCollection Character = new(); public static readonly DelegateCollection Monster = new(); + public static readonly DelegateCollection Companion = new(); public static readonly DelegateCollection Skill = new(); public static readonly DelegateCollection Combat = new(); public static readonly DelegateCollection SkillHit = new(); @@ -126,6 +127,13 @@ public ScriptableFunctionAttribute(string scriptFuncName) /// public delegate float MonsterCalcFunc(Mob monster); + /// + /// A function that calculates a value for a companion. + /// + /// + /// + public delegate float CompanionCalcFunc(Companion companion); + /// /// A function that calculates a value for a skill. /// diff --git a/src/ZoneServer/World/Actors/Characters/Character.cs b/src/ZoneServer/World/Actors/Characters/Character.cs index 7a913508b..62cc92a1c 100644 --- a/src/ZoneServer/World/Actors/Characters/Character.cs +++ b/src/ZoneServer/World/Actors/Characters/Character.cs @@ -352,6 +352,11 @@ public int JobLevel /// public MovementComponent Movement { get; } + /// + /// Returns the character's companion component. + /// + public CompanionComponent Companions { get; } + /// /// Character's properties. /// @@ -405,6 +410,7 @@ public Character() : base() this.Components.Add(this.Quests = new QuestComponent(this)); this.Components.Add(this.Collections = new CollectionComponent(this)); this.Components.Add(this.Movement = new MovementComponent(this)); + this.Components.Add(this.Companions = new CompanionComponent(this)); this.Properties = new CharacterProperties(this); this.Etc = new PCEtc(this); diff --git a/src/ZoneServer/World/Actors/Characters/Components/CompanionComponent.cs b/src/ZoneServer/World/Actors/Characters/Components/CompanionComponent.cs new file mode 100644 index 000000000..a52938bf6 --- /dev/null +++ b/src/ZoneServer/World/Actors/Characters/Components/CompanionComponent.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Melia.Zone.Network; +using Melia.Zone.World.Actors.Monsters; + +namespace Melia.Zone.World.Actors.Characters.Components +{ + public class CompanionComponent : CharacterComponent + { + private readonly List _companions = new List(); + + public CompanionComponent(Character character) : base(character) + { + } + + public bool HasCompanions + { + get + { + lock (_companions) + return _companions.Count > 0; + } + } + + public Companion ActiveCompanion + { + get + { + lock (_companions) + return _companions?.Find(companion => companion.IsActivated); + } + } + + /// Add companion + /// + /// + /// + public void AddCompanion(Companion companion, bool silently = false) + { + lock (_companions) + this._companions.Add(companion); + if (!silently) + Send.ZC_NORMAL.PetInfo(this.Character); + } + + /// + /// Adds companion to character and the database. + /// + /// + public void CreateCompanion(Companion companion) + { + ZoneServer.Instance.Database.CreateCompanion(this.Character.AccountId, this.Character.DbId, companion); + this.AddCompanion(companion); + } + + /// + /// Get Companions + /// + /// + public IList GetList() + { + lock (_companions) + return _companions; + } + + /// + /// Get Companions + /// + /// + public IList GetList(Func predicate) + { + lock (_companions) + return _companions.Where(predicate).ToList(); + } + + /// + /// Returns companion or null with a given id. + /// + /// + /// + public Companion GetCompanion(long companionId) + { + lock (_companions) + return _companions.Find(c => c.ObjectId == companionId); + } + + /// + /// Returns companion or null with a given id. + /// + /// + /// + /// + public bool TryGetCompanion(Predicate predicate, out Companion companion) + { + lock (_companions) + companion = _companions.Find(predicate); + return companion != null; + } + } +} diff --git a/src/ZoneServer/World/Actors/Monsters/Companion.cs b/src/ZoneServer/World/Actors/Monsters/Companion.cs new file mode 100644 index 000000000..662aace07 --- /dev/null +++ b/src/ZoneServer/World/Actors/Monsters/Companion.cs @@ -0,0 +1,238 @@ +using System; +using Melia.Shared.Data.Database; +using Melia.Shared.ObjectProperties; +using Melia.Shared.Game.Const; +using Melia.Shared.Util; +using Melia.Shared.World; +using Melia.Zone.Network; +using Melia.Zone.Scripting; +using Melia.Zone.World.Actors.Characters; +using Melia.Zone.World.Actors.CombatEntities.Components; +using Yggdrasil.Scheduling; +using System.Threading; + +namespace Melia.Zone.World.Actors.Monsters +{ + public class Companion : Mob, IPropertyObject + { + /// + /// Companion's unique id. + /// + public long DbId { get; set; } + + /// + /// Companion's globally unique id. + /// + public long ObjectId => ObjectIdRanges.Companions + this.DbId; + + /// + /// A reference to the character which owns this companion. + /// + public Character Owner { get; private set; } + + /// + /// Companion is activated (Visible) + /// + public bool IsActivated + { + get + { + return this.Properties.GetFloat(PropertyName.IsActivated) == 1; + } + set + { + this.Properties.SetFloat(PropertyName.IsActivated, value ? 1 : 0); + } + } + + /// + /// Current stamina. + /// + public int Stamina { get; set; } + /// + /// Current experience points. + /// + public long Exp { get; set; } + + /// + /// Current maximum experience points. + /// + public long MaxExp { get; set; } + + /// + /// Total number of accumulated experience points. + /// + public long TotalExp { get; set; } + + /// + /// Adopt Time + /// + public DateTime AdoptTime { get; set; } + + /// + /// Return if the companion is being ride on or not. + /// + public bool IsRiding { get; set; } = false; + + public CompanionData CompanionData { get; private set; } + + public Companion(Character owner, int id) : base(id, MonsterType.Friendly) + { + this.Owner = owner; + + if (!ZoneServer.Instance.Data.CompanionDb.TryFindByClassName(this.Data.ClassName, out var companionData)) + throw new NullReferenceException("No companion data found for '" + this.Data.ClassName + "'."); + this.CompanionData = companionData; + + this.InitProperties(); + } + + /// + /// Initializes companion's properties. + /// + private void InitProperties() + { + this.CreateProperty(PropertyName.STR, "SCR_Get_Companion_STR"); + this.CreateProperty(PropertyName.DEX, "SCR_Get_Companion_DEX"); + this.CreateProperty(PropertyName.CON, "SCR_Get_Companion_CON"); + this.CreateProperty(PropertyName.INT, "SCR_Get_Companion_INT"); + this.CreateProperty(PropertyName.MNA, "SCR_Get_Companion_MNA"); + this.CreateProperty(PropertyName.DEF, "SCR_Get_Companion_DEF"); + this.CreateProperty(PropertyName.MDEF, "SCR_Get_Companion_MDEF"); + this.CreateProperty(PropertyName.MHP, "SCR_Get_Companion_MHP"); + + this.CreateProperty(PropertyName.MAXPATK, "SCR_Get_Companion_MAXPATK"); + this.CreateProperty(PropertyName.MINPATK, "SCR_Get_Companion_MINPATK"); + this.CreateProperty(PropertyName.MAXMATK, "SCR_Get_Companion_MAXMATK"); + this.CreateProperty(PropertyName.MINMATK, "SCR_Get_Companion_MINMATK"); + this.CreateProperty(PropertyName.ATK, "SCR_Get_Companion_ATK"); + + this.CreateProperty(PropertyName.MountDEF, "SCR_Get_Companion_MOUNTDEF"); + this.CreateProperty(PropertyName.MountDR, "SCR_Get_Companion_MOUNTDR"); + this.CreateProperty(PropertyName.MountMHP, "SCR_Get_Companion_MOUNTMHP"); + + this.Properties.InitAutoUpdates(); + this.Properties.AutoUpdate(PropertyName.MHP, [PropertyName.Lv, PropertyName.Level, PropertyName.MHP_BM]); + this.Properties.AutoUpdate(PropertyName.MINPATK, [PropertyName.Lv, PropertyName.Level, PropertyName.PATK_BM]); + this.Properties.AutoUpdate(PropertyName.MAXPATK, [PropertyName.Lv, PropertyName.Level, PropertyName.PATK_BM]); + this.Properties.AutoUpdate(PropertyName.MINMATK, [PropertyName.Lv, PropertyName.Level, PropertyName.MATK_BM]); + this.Properties.AutoUpdate(PropertyName.MAXMATK, [PropertyName.Lv, PropertyName.Level, PropertyName.MATK_BM]); + this.Properties.AutoUpdate(PropertyName.ATK, [PropertyName.Lv, PropertyName.Level]); + this.Properties.AutoUpdate(PropertyName.DEF, [PropertyName.Lv, PropertyName.Level, PropertyName.DEF_BM]); + this.Properties.AutoUpdate(PropertyName.MDEF, [PropertyName.Lv, PropertyName.Level, PropertyName.MDEF_BM]); + + this.Properties.InvalidateAll(); + + this.Properties.SetFloat(PropertyName.HP, this.Properties.GetFloat(PropertyName.MHP)); + this.Properties.SetFloat(PropertyName.SP, this.Properties.GetFloat(PropertyName.MSP)); + } + + /// + /// Creates a new calculated float property that uses the given + /// function. + /// + /// + /// + private void CreateProperty(string propertyName, string calcFuncName) + { + this.Properties.Create(new CFloatProperty(propertyName, () => this.CalculateProperty(calcFuncName))); + } + + /// + /// Calls the calculation function with the given name and returns + /// the result. + /// + /// + /// + /// + /// Thrown if the function doesn't exist. + /// + private float CalculateProperty(string calcFuncName) + { + if (!ScriptableFunctions.Companion.TryGet(calcFuncName, out var func)) + throw new ArgumentException($"Scriptable character function '{calcFuncName}' not found."); + + return func(this); + } + + public void SetCompanionState(bool isActive) + { + this.IsActivated = isActive; + Send.ZC_OBJECT_PROPERTY(this.Owner.Connection, this, PropertyName.IsActivated); + if (isActive) + { + this.Map = this.Owner.Map; + this.OwnerHandle = this.Owner.Handle; + this.Position = this.Owner.Position.GetRandomInRange2D(15, new Random()); + this.Components.Add(new MovementComponent(this)); + this.Components.Add(new AiComponent(this, "BasicMonster")); + this.Components.Get()?.Script.SetMaster(this.Owner); + Send.ZC_NORMAL.PetIsInactive(this.Owner.Connection, this); + this.Map.AddMonster(this); + //Send.ZC_NORMAL.PetPlayAnimation(this.Owner.Connection, this); + } + else + { + this.Components.Remove(); + this.Components.Remove(); + this.Position = Position.Zero; + this.OwnerHandle = 0; + // Clear Buffs + Send.ZC_LEAVE(this.Owner, 4); + this.Map.RemoveMonster(this); + } + } + + /// + /// Grants exp to companion. + /// + /// + /// + public void GiveExp(long exp, IMonsterBase monster) + { + // Base EXP + this.Exp += exp; + this.TotalExp += exp; + + Send.ZC_NORMAL.PetExpUpdate(this.Owner, this); + + var level = this.Level; + var levelUps = 0; + var maxExp = this.MaxExp; + var maxLevel = ZoneServer.Instance.Data.ExpDb.GetMaxLevel(); + + // Consume EXP as many times as possible to reach new levels + while (this.Exp >= maxExp && level < maxLevel) + { + this.Exp -= maxExp; + + level++; + levelUps++; + maxExp = ZoneServer.Instance.Data.ExpDb.GetNextExp(level); + } + + // Execute level up only once to avoid client lag on multiple + // level ups. Leveling up a thousand times in a loop is not + // fun for the client =D" + if (levelUps > 0) + this.LevelUp(levelUps); + } + + /// + /// Increases companion's level by the given amount. + /// + /// + public void LevelUp(int amount = 1) + { + if (amount < 1) + throw new ArgumentException("Amount can't be lower than 1."); + + var newLevel = this.Properties.Modify(PropertyName.Lv, amount); + + this.MaxExp = ZoneServer.Instance.Data.ExpDb.GetNextExp((int)newLevel); + this.Heal(this.MaxHp, 0); + + Send.ZC_NORMAL.PlayEffect(this, "F_companion_level_up", 3); + } + } +} diff --git a/src/ZoneServer/World/Actors/Monsters/Mob.cs b/src/ZoneServer/World/Actors/Monsters/Mob.cs index d8f9e8a3c..4483a5bda 100644 --- a/src/ZoneServer/World/Actors/Monsters/Mob.cs +++ b/src/ZoneServer/World/Actors/Monsters/Mob.cs @@ -208,6 +208,17 @@ public class Mob : Actor, IMonster, ICombatEntity, IUpdateable /// public ConcurrentBag StaticDrops { get; } = new ConcurrentBag(); + /// + /// Gets or sets the handled who is the owner. + /// + public int OwnerHandle { get; set; } + + /// + /// Gets or sets the handle associated + /// with the spawn. + /// + public int AssociatedHandle { get; set; } + /// /// Creates new NPC. /// diff --git a/src/ZoneServer/World/Actors/Monsters/Monster.cs b/src/ZoneServer/World/Actors/Monsters/Monster.cs index 526f2ef24..6570923ef 100644 --- a/src/ZoneServer/World/Actors/Monsters/Monster.cs +++ b/src/ZoneServer/World/Actors/Monsters/Monster.cs @@ -54,6 +54,16 @@ public interface IMonsterBase : IActor, IMonsterAppearance, IMonsterAppearanceBa /// Returns a reference to the monster's properties. /// Properties Properties { get; } + + /// + /// Returns the owner's handle. + /// + int OwnerHandle { get; } + + /// + /// Returns an associated handle. + /// + int AssociatedHandle { get; } } /// diff --git a/src/ZoneServer/World/Actors/Monsters/MonsterInName.cs b/src/ZoneServer/World/Actors/Monsters/MonsterInName.cs index 62c316c92..6cd2cf5c4 100644 --- a/src/ZoneServer/World/Actors/Monsters/MonsterInName.cs +++ b/src/ZoneServer/World/Actors/Monsters/MonsterInName.cs @@ -104,6 +104,17 @@ public abstract class MonsterInName : Actor, IMonster /// public string LeaveName { get; set; } + /// + /// Gets or sets the handled who is the owner. + /// + public int OwnerHandle { get; set; } + + /// + /// Gets or sets the handle associated + /// with the spawn. + /// + public int AssociatedHandle { get; set; } + /// /// Initializes the monster's properties. /// diff --git a/system/conf/commands.conf b/system/conf/commands.conf index 9f55aff87..fbe927f0a 100644 --- a/system/conf/commands.conf +++ b/system/conf/commands.conf @@ -36,6 +36,8 @@ buyabilpoint : 0,-1 learnpcabil : 0,-1 intewarpByToken : 0,-1 mic : 0,-1 +pethire : 0,-1 +petstat : 0,-1 // Custom client commands // These commands are used by our internal custom scripts to communicate diff --git a/system/db/companions.txt b/system/db/companions.txt new file mode 100644 index 000000000..22910578d --- /dev/null +++ b/system/db/companions.txt @@ -0,0 +1,71 @@ +// Melia +// Database file +// +// For reference only. +// Values: id, className, name, rideMSPD, isPremium, canPet, canRide, endRideOnHit, [foodGroup], [buff] +//--------------------------------------------------------------------------- + +[ +{ id: 1, className: "GameStudio_None", name: "", rideMSPD: 0, isPremium: false, canPet: false, canRide: false, endRideOnHit: true, feedAnimation: 1, feedSleep: 3000 }, +{ id: 2, className: "Mon_alpaka", name: "Alpaca", rideMSPD: 0, isPremium: false, canPet: true, canRide: false, endRideOnHit: true, feedAnimation: 2, feedSleep: 3000 }, +{ id: 3, className: "Velhider", name: "Velheider", price: 110000, rideMSPD: 4, isPremium: false, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, feedAnimation: 3, feedSleep: 3500 }, +{ id: 4, className: "hoglan_Pet", name: "Hoglan", price: 453600, rideMSPD: 4, isPremium: false, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, feedAnimation: 4, feedSleep: 3500 }, +{ id: 5, className: "pet_hawk", name: "Hawk", price: 453600, rideMSPD: 4, isPremium: false, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, feedAnimation: 19, feedSleep: 3000 }, +{ id: 6, className: "Piggy", name: "Pig", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, feedAnimation: 6, feedSleep: 3000 }, +{ id: 7, className: "Lesser_panda", name: "Lesser Panda", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 5, buff: "pet_Lesserpanda_buff", feedAnimation: 7, feedSleep: 3500 }, +{ id: 8, className: "Toucan", name: "Toucan", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 8, feedAnimation: 9, feedSleep: 1500 }, +{ id: 9, className: "Guineapig", name: "Guinea Pig", rideMSPD: 4, isPremium: false, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 9, feedAnimation: 10, feedSleep: 2700 }, +{ id: 10, className: "barn_owl", name: "Owl", rideMSPD: 4, isPremium: false, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 10, feedAnimation: 11, feedSleep: 3000 }, +{ id: 11, className: "Piggy_baby", name: "Pig", rideMSPD: 0, isPremium: false, canPet: false, canRide: false, endRideOnHit: true, foodGroup: 6, feedAnimation: 12, feedSleep: 3000 }, +{ id: 12, className: "Lesser_panda_baby", name: "Lesser Panda", rideMSPD: 0, isPremium: false, canPet: false, canRide: false, endRideOnHit: true, foodGroup: 5, feedAnimation: 13, feedSleep: 3000 }, +{ id: 13, className: "guineapig_baby", name: "Guinea Pig", rideMSPD: 0, isPremium: false, canPet: false, canRide: false, endRideOnHit: true, foodGroup: 0, feedAnimation: 14, feedSleep: 3000 }, +{ id: 14, className: "penguin", name: "Penguin", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 11, buff: "pet_penguin_buff", feedAnimation: 15, feedSleep: 1500 }, +{ id: 15, className: "parrotbill", name: "Battlebird", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 12, buff: "pet_parrotbill_buff", feedAnimation: 17, feedSleep: 2700, feedCreateTime: 500, feedDistance: 12 }, +{ id: 16, className: "parrotbill_dummy", name: "Battlebird", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 12 }, +{ id: 17, className: "Pet_Rocksodon", name: "Rocksodon", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, buff: "pet_rocksodon_buff", feedAnimation: 18, feedSleep: 3500 }, +{ id: 18, className: "penguin_green", name: "Leaf Penguin", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 11, feedAnimation: 20, feedSleep: 1500 }, +{ id: 19, className: "PetHanaming", name: "", rideMSPD: 0, isPremium: false, canPet: false, canRide: false, endRideOnHit: true, buff: "pet_PetHanaming_buff" }, +{ id: 20, className: "penguin_marine", name: "Pengmarine", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 11, feedAnimation: 21, feedSleep: 1500 }, +{ id: 21, className: "Lesser_panda_gray", name: "Gray Lesser Panda", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 5, buff: "pet_Lesserpanda_gray_buff" }, +{ id: 22, className: "pet_sled", name: "Christmas Sled", rideMSPD: 6, isPremium: true, canPet: false, canRide: true, endRideOnHit: false }, +{ id: 23, className: "Armadillo", name: "Armadillo", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, feedAnimation: 23, feedSleep: 1500 }, +{ id: 24, className: "pet_goro_suit", name: "Goro", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, buff: "PET_GORO_BUFF", feedAnimation: 24, feedSleep: 1500 }, +{ id: 25, className: "Pet_Golddog", name: "Golden Dog", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, buff: "PET_GOLD_DOG_BUFF", feedAnimation: 25, feedSleep: 1500 }, +{ id: 26, className: "pet_winter_penguine", name: "Winter Penguin", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 11, feedAnimation: 26, feedSleep: 1500 }, +{ id: 27, className: "Pet_mrdodo", name: "Dodo", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 5, feedAnimation: 27, feedSleep: 1500 }, +{ id: 28, className: "pet_weddingbird", name: "Fairy Cardinal", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 12, buff: "PET_WEDDING_BIRD_BUFF", feedAnimation: 28, feedSleep: 2700, feedCreateTime: 500, feedDistance: 12 }, +{ id: 29, className: "Pet_piggy_spotted", name: "Spotted Baby Pig", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, feedAnimation: 29, feedSleep: 3000 }, +{ id: 30, className: "pet_goro_suit2", name: "Goro", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, buff: "PET_GORO_BUFF2", feedAnimation: 30, feedSleep: 1500 }, +{ id: 31, className: "royaltemplar_horse", name: "Templar Horse", rideMSPD: 4, isPremium: false, canPet: false, canRide: true, endRideOnHit: false, foodGroup: 0 }, +{ id: 32, className: "guineapig_white", name: "White Guinea Pig", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 9 }, +{ id: 33, className: "Pet_gold_pig", name: "Lucky Golden Pig", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, buff: "PET_GOLD_PIG_BUFF", feedAnimation: 31, feedSleep: 3000 }, +{ id: 34, className: "pet_chick_crowtit", name: "Teeny Chick", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 12, buff: "pet_parrotbill_buff", feedAnimation: 32, feedSleep: 2700, feedCreateTime: 500, feedDistance: 12 }, +{ id: 35, className: "pet_dionys", name: "Dionys Cub", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, buff: "pet_dionys_buff", feedAnimation: 33, feedSleep: 1500 }, +{ id: 36, className: "pet_sparrow", name: "Fluffy Sparrow.", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 12, buff: "pet_sparrow_buff", feedAnimation: 34, feedSleep: 2700, feedCreateTime: 500, feedDistance: 12 }, +{ id: 37, className: "pet_school_dodo", name: "School Dodo", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 11, buff: "pet_school_dodo_buff", feedAnimation: 35, feedSleep: 2700, feedCreateTime: 500, feedDistance: 12 }, +{ id: 38, className: "pet_hedgehog", name: "HedgeHog", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, buff: "pet_hedgehog_buff", feedAnimation: 36, feedSleep: 2700, feedCreateTime: 500, feedDistance: 12 }, +{ id: 39, className: "pet_policedog", name: "Police Shepherd", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, buff: "pet_policedog_buff", feedAnimation: 37, feedSleep: 2700, feedCreateTime: 500, feedDistance: 12 }, +{ id: 40, className: "pet_cadet_armadillo", name: "Cadet Armadillo", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, buff: "pet_cadet_armadillo_buff", feedAnimation: 38, feedSleep: 1500 }, +{ id: 41, className: "pet_new_penguin", name: "Baby Penguin", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 11, buff: "pet_penguin_buff", feedAnimation: 39, feedSleep: 1500 }, +{ id: 42, className: "pet_sparrow_Thanksgiving", name: "Moonlight Sparrow", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 12, buff: "pet_sparrow_thanksgivng_buff", feedAnimation: 40, feedSleep: 1500, feedCreateTime: 500, feedDistance: 12 }, +{ id: 43, className: "pet_dalmatian", name: "Dalmatian", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, buff: "pet_dalmatian_buff", feedAnimation: 41, feedSleep: 2700, feedCreateTime: 500, feedDistance: 12 }, +{ id: 44, className: "pet_winter_rabbit", name: "Snow Bunny", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, buff: "pet_winter_rabbit_buff", feedAnimation: 42, feedSleep: 3500 }, +{ id: 45, className: "pet_twnpanda", name: "Panda", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, buff: "pet_twnpanda_buff", feedAnimation: 43, feedSleep: 5000 }, +{ id: 46, className: "pet_robot_dog", name: "Assistant Robot Puppy", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, buff: "pet_robot_dog_buff", feedAnimation: 44, feedSleep: 2700 }, +{ id: 47, className: "pet_ep13dessert", name: "Sweet Pudding Penguin ", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 11, buff: "pet_ep13dessert_buff", feedAnimation: 45, feedSleep: 1500 }, +{ id: 48, className: "pet_arborday_rabbit", name: "Botanic Bunny", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, buff: "pet_arborday_rabbit_buff", feedAnimation: 46, feedSleep: 3500 }, +{ id: 49, className: "pet_twnocelot", name: "Ocelot", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, buff: "pet_twnocelot_buff", feedAnimation: 47, feedSleep: 1500 }, +{ id: 50, className: "Pet_Sled_blue", name: "Plateau Sled", rideMSPD: 6, isPremium: true, canPet: false, canRide: true, endRideOnHit: false }, +{ id: 51, className: "pet_ep13ge", name: "Noble Popo", rideMSPD: 6, isPremium: true, canPet: false, canRide: true, endRideOnHit: false }, +{ id: 52, className: "pet_skunk", name: "Skunk", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, buff: "pet_skunk_buff", feedAnimation: 48, feedSleep: 1500 }, +{ id: 53, className: "pet_aircraft", name: "ToS-15 Takeoff!", rideMSPD: 6, isPremium: true, canPet: false, canRide: true, endRideOnHit: false }, +{ id: 54, className: "pet_twn6th_TI", name: "Malayan Tapir", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, buff: "pet_twn6th_TI_buff", feedAnimation: 49, feedSleep: 1500 }, +{ id: 55, className: "pet_twnocelot_black", name: "Baby Black Leopard", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, buff: "pet_twnocelot_black_buff", feedAnimation: 50, feedSleep: 1500 }, +{ id: 56, className: "pet_twnocelot_white", name: "Baby Snow Leopard", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, buff: "pet_twnocelot_white_buff", feedAnimation: 51, feedSleep: 1500 }, +{ id: 57, className: "Pet_Sled_skyblue", name: "Sky Blue Sled", rideMSPD: 6, isPremium: true, canPet: false, canRide: true, endRideOnHit: false }, +{ id: 58, className: "pet_jpn3th_fox", name: "Red Fox", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, buff: "pet_jpn3th_fox_buff", feedAnimation: 52, feedSleep: 1500 }, +{ id: 59, className: "Pet_Sled_green", name: "Fresh Green Sled", rideMSPD: 6, isPremium: true, canPet: false, canRide: true, endRideOnHit: false }, +{ id: 60, className: "pet_nightrabbit", name: "Midnight Bunny", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, buff: "pet_nightrabbit_buff" }, +{ id: 61, className: "pet_snigo", name: "Shape Memory Snigo", rideMSPD: 6, isPremium: true, canPet: false, canRide: true, endRideOnHit: false, buff: "pet_snigo_buff", feedAnimation: 53, feedSleep: 3500 }, +{ id: 62, className: "pet_Armadillo_wingedhussar", name: "Armadillo", rideMSPD: 4, isPremium: true, canPet: false, canRide: true, endRideOnHit: true, foodGroup: 0, feedAnimation: 54, feedSleep: 1500 }, +] \ No newline at end of file diff --git a/system/scripts/zone/core/calc_companion.cs b/system/scripts/zone/core/calc_companion.cs new file mode 100644 index 000000000..1322c1e8f --- /dev/null +++ b/system/scripts/zone/core/calc_companion.cs @@ -0,0 +1,394 @@ +//--- Melia Script ---------------------------------------------------------- +// Companion Calculation Script +//--- Description ----------------------------------------------------------- +// Functions that calculate companion-related values, such as properties. +//--------------------------------------------------------------------------- + +using System; +using Melia.Shared.Game.Const; +using Melia.Zone.Scripting; +using Melia.Zone.World.Actors; +using Melia.Zone.World.Actors.Monsters; +using Melia.Zone.World.Maps; + +public class CompanionCalculationsScript : GeneralScript +{ + private const float PET_STAT_BY_OWNER_RATE = 0.5f; + + public float PET_STAT_BY_OWNER(Companion companion, string statName) + { + var value = 0f; + var owner = companion.Owner; + + if (owner != null) + value = owner.Properties.GetFloat(statName) * PET_STAT_BY_OWNER_RATE; + + return (float)Math.Floor(value); + } + + /// + /// Returns companion's total strength. + /// + /// + /// + [ScriptableFunction] + public float SCR_Get_Companion_STR(Companion companion) + { + var baseValue = companion.Data.STR; + var ownerValue = PET_STAT_BY_OWNER(companion, PropertyName.STR); + + var result = baseValue + ownerValue; + + return (float)Math.Floor(result); + } + + /// + /// Returns companion's total dexterity. + /// + /// + /// + [ScriptableFunction] + public float SCR_Get_Companion_DEX(Companion companion) + { + var ownerProperties = companion.Owner.Properties; + + var baseValue = companion.Data.DEX; + var ownerValue = ownerProperties.GetFloat(PropertyName.DEX) * PET_STAT_BY_OWNER_RATE; + + var result = baseValue + ownerValue; + + return (float)Math.Floor(result); + } + + /// + /// Returns companion's total constitution. + /// + /// + /// + [ScriptableFunction] + public float SCR_Get_Companion_CON(Companion companion) + { + var ownerProperties = companion.Owner.Properties; + + var baseValue = companion.Data.CON; + var ownerValue = ownerProperties.GetFloat(PropertyName.CON) * PET_STAT_BY_OWNER_RATE; + + var result = baseValue + ownerValue; + + return (float)Math.Floor(result); + } + + /// + /// Returns companion's total intelligence. + /// + /// + /// + [ScriptableFunction] + public float SCR_Get_Companion_INT(Companion companion) + { + var ownerProperties = companion.Owner.Properties; + + var baseValue = companion.Data.INT; + var ownerValue = ownerProperties.GetFloat(PropertyName.INT) * PET_STAT_BY_OWNER_RATE; + + var result = baseValue + ownerValue; + + return (float)Math.Floor(result); + } + + /// + /// Returns companion's total mana. + /// + /// + /// + [ScriptableFunction] + public float SCR_Get_Companion_MNA(Companion companion) + { + var ownerProperties = companion.Owner.Properties; + + var baseValue = companion.Data.MNA; + var ownerValue = ownerProperties.GetFloat(PropertyName.MNA) * PET_STAT_BY_OWNER_RATE; + + var result = baseValue + ownerValue; + + return (float)Math.Floor(result); + } + + /// + /// Returns companion's calculated defense. + /// + /// + /// + [ScriptableFunction] + public float SCR_Get_Companion_DEF(Companion companion) + { + var properties = companion.Properties; + var byLv = properties.GetFloat(PropertyName.Lv); + var addLv = properties.GetFloat(PropertyName.Level); + var byOwner = PET_STAT_BY_OWNER(companion, PropertyName.DEF); + var value = (byLv + addLv) / 2f + byOwner + properties.GetFloat(PropertyName.Stat_DEF); + + var owner = companion.Owner; + if (owner != null) + { + if (owner.IsAbilityActive(AbilityId.CompMastery4)) + value *= 1.25f; + if (owner.IsAbilityActive(AbilityId.CompMastery5)) + value *= 0.75f; + } + + return (float)Math.Floor(Math.Max(1, value)); + } + + /// + /// Returns companion's calculated magic defense. + /// + /// + /// + [ScriptableFunction] + public float SCR_Get_Companion_MDEF(Companion companion) + { + var properties = companion.Properties; + var byLv = properties.GetFloat(PropertyName.Lv); + var addLv = properties.GetFloat(PropertyName.Level); + var byOwner = PET_STAT_BY_OWNER(companion, PropertyName.MDEF); + var value = (byLv + addLv) / 2f + byOwner + properties.GetFloat(PropertyName.Stat_MDEF); + + var owner = companion.Owner; + if (owner != null) + { + if (owner.IsAbilityActive(AbilityId.CompMastery4)) + value *= 1.25f; + if (owner.IsAbilityActive(AbilityId.CompMastery5)) + value *= 0.75f; + } + + return (float)Math.Floor(Math.Max(1, value)); + } + + /// + /// Returns companion's calculated dodge rate. + /// + /// + /// + [ScriptableFunction] + public float SCR_Get_Companion_DR(Companion companion) + { + var properties = companion.Properties; + var byLv = properties.GetFloat(PropertyName.Lv); + var addLv = properties.GetFloat(PropertyName.Level); + var byOwner = PET_STAT_BY_OWNER(companion, PropertyName.DR); + var value = byLv + addLv + byOwner + properties.GetFloat("DEX") + properties.GetFloat(PropertyName.Stat_DR); + + return (float)Math.Floor(value); + } + + /// + /// Returns companion's calculated hit rate. + /// + /// + /// + [ScriptableFunction] + public float SCR_Get_Companion_HR(Companion companion) + { + var properties = companion.Properties; + var byLv = properties.GetFloat(PropertyName.Lv); + var addLv = properties.GetFloat(PropertyName.Level); + var byOwner = PET_STAT_BY_OWNER(companion, PropertyName.HR); + var value = byLv + addLv + byOwner + properties.GetFloat("DEX") + properties.GetFloat(PropertyName.Stat_DR) + properties.GetFloat(PropertyName.Stat_HR_BM); + + return (float)Math.Floor(value); + } + + /// + /// Returns the companion's maximum HP. + /// + /// + /// + [ScriptableFunction] + public float SCR_Get_Companion_MHP(Companion companion) + { + var properties = companion.Properties; + + var level = properties.GetFloat(PropertyName.Lv, 1); + var standardMHP = level * 10; + var stat = properties.GetFloat(PropertyName.CON, 1); + + var byLevel = Math.Floor((standardMHP / 4) * level); + var byStat = Math.Floor((byLevel * (stat * 0.0015)) + (byLevel * (Math.Floor(stat / 10) * 0.005)) + (27 * properties.GetFloat(PropertyName.Stat_MHP))); + + var value = byLevel + byStat; + + var owner = companion.Owner; + if (owner != null) + { + if (owner.IsAbilityActive(AbilityId.CompMastery4)) + value *= 1.25f; + if (owner.IsAbilityActive(AbilityId.CompMastery5)) + value *= 0.75f; + } + if (companion.IsBuffActive(BuffId.BeastMaster_Buff)) + value *= 1.25f; + + return (float)Math.Floor(Math.Max(1, value)); + } + + /// + /// Returns the companion's attack. + /// + /// + /// + [ScriptableFunction] + public float SCR_Get_Companion_ATK(Companion companion) + { + var properties = companion.Properties; + + var addLv = companion.Data.Level; + var atk = properties.GetFloat(PropertyName.Lv) + companion.Data.STR + addLv + properties.GetFloat(PropertyName.Stat_ATK) + properties.GetFloat(PropertyName.Stat_ATK_BM); + + var average = PET_STAT_BY_OWNER(companion, PropertyName.MINPATK) + PET_STAT_BY_OWNER(companion, PropertyName.MAXPATK); + if (average != 0) + average /= 2; + + var value = atk + average; + + var owner = companion.Owner; + if (owner != null) + { + if (owner.IsAbilityActive(AbilityId.CompMastery4)) + value *= 1.25f; + if (owner.IsAbilityActive(AbilityId.CompMastery5)) + value *= 0.75f; + } + + return (float)Math.Floor(Math.Max(1, value)); + } + + /// + /// Returns the companion's minimum physical attack. + /// + /// + /// + [ScriptableFunction] + public float SCR_Get_Companion_MINPATK(Companion companion) + { + var properties = companion.Properties; + var byStat = companion.Data.PhysicalAttackMin; + var byBuff = properties.GetFloat(PropertyName.PATK_BM); + var value = byStat + byBuff; + + return (float)Math.Floor(value); + } + + /// + /// Returns the companion's maximum physical attack. + /// + /// + /// + [ScriptableFunction] + public float SCR_Get_Companion_MAXPATK(Companion companion) + { + var properties = companion.Properties; + var byStat = companion.Data.PhysicalAttackMax; + var byBuff = properties.GetFloat(PropertyName.PATK_BM); + var value = byStat + byBuff; + + return (float)Math.Floor(value); + } + + /// + /// Returns the companion's minimum magic attack. + /// + /// + /// + [ScriptableFunction] + public float SCR_Get_Companion_MINMATK(Companion companion) + { + var properties = companion.Properties; + var byStat = companion.Data.MagicalAttackMin; + var byOwner = PET_STAT_BY_OWNER(companion, PropertyName.MINMATK); + var byBuff = properties.GetFloat(PropertyName.PATK_BM); + var value = byStat + byOwner + byBuff; + + var owner = companion.Owner; + if (owner != null) + { + if (owner.IsAbilityActive(AbilityId.CompMastery4)) + value *= 1.25f; + if (owner.IsAbilityActive(AbilityId.CompMastery5)) + value *= 0.75f; + } + + return (float)Math.Floor(Math.Max(1, value)); + } + + /// + /// Returns the companion's maximum magical attack. + /// + /// + /// + [ScriptableFunction] + public float SCR_Get_Companion_MAXMATK(Companion companion) + { + var properties = companion.Properties; + var byStat = companion.Data.MagicalAttackMax; + var byOwner = PET_STAT_BY_OWNER(companion, PropertyName.MAXMATK); + var byBuff = properties.GetFloat(PropertyName.PATK_BM); + var value = byStat + byOwner + byBuff; + + var owner = companion.Owner; + if (owner != null) + { + if (owner.IsAbilityActive(AbilityId.CompMastery4)) + value *= 1.25f; + if (owner.IsAbilityActive(AbilityId.CompMastery5)) + value *= 0.75f; + } + + return (float)Math.Floor(Math.Max(1, value)); + } + + /// + /// Returns companion's mount defense. + /// + /// + /// + [ScriptableFunction] + public float SCR_Get_Companion_MOUNTDEF(Companion companion) + { + return (float)Math.Floor(companion.Properties.CFloat("DEF") * 0.1); + } + + /// + /// Returns companion's mount dodge rate. + /// + /// + /// + [ScriptableFunction] + public float SCR_Get_Companion_MOUNTDR(Companion companion) + { + return (float)Math.Floor(companion.Properties.CFloat("DR") * 0.08); + } + + /// + /// Returns companion's mount max hp. + /// + /// + /// + [ScriptableFunction] + public float SCR_Get_Companion_MOUNTMHP(Companion companion) + { + return (float)Math.Floor(companion.Properties.CFloat("MHP") * 0.25); + } + + /// + /// Returns companion's total mana. + /// + /// + /// + [ScriptableFunction] + public float SCR_Get_Companion_SDR(Companion companion) + { + return 1f; + } +}