diff --git a/excel/cha.xlsx b/excel/cha.xlsx new file mode 100644 index 0000000..5552c54 Binary files /dev/null and b/excel/cha.xlsx differ diff --git a/excel/enemy.xlsx b/excel/enemy.xlsx index bc9b0ad..6332a8c 100644 Binary files a/excel/enemy.xlsx and b/excel/enemy.xlsx differ diff --git a/src/ReplicatedStorage/Json/Character.json b/src/ReplicatedStorage/Json/Character.json new file mode 100644 index 0000000..9b0036b --- /dev/null +++ b/src/ReplicatedStorage/Json/Character.json @@ -0,0 +1,3 @@ +[ +{"id":1,"name":1,"attack":10,"hp":100,"walkSpeed":10,"attackSpeed":2} +] \ No newline at end of file diff --git a/src/ReplicatedStorage/Json/Enemy.json b/src/ReplicatedStorage/Json/Enemy.json index cd2d6ad..05a2143 100644 --- a/src/ReplicatedStorage/Json/Enemy.json +++ b/src/ReplicatedStorage/Json/Enemy.json @@ -1,5 +1,5 @@ [ -{"id":1,"type":1,"name":1,"atk":10,"hp":100,"walkSpeed":10,"atkSpeed":2,"model":"Thief"}, -{"id":2,"type":1,"name":2,"atk":30,"hp":300,"walkSpeed":10,"atkSpeed":1,"model":"Thief"}, -{"id":1000,"type":2,"name":1000,"atk":50,"hp":1000,"walkSpeed":20,"atkSpeed":1,"model":"Thief"} +{"id":1,"type":1,"name":1,"attack":10,"hp":100,"walkSpeed":10,"attackSpeed":2,"model":"Thief"}, +{"id":2,"type":1,"name":2,"attack":30,"hp":300,"walkSpeed":10,"attackSpeed":1,"model":"Thief"}, +{"id":1000,"type":2,"name":1000,"attack":50,"hp":1000,"walkSpeed":20,"attackSpeed":1,"model":"Thief"} ] \ No newline at end of file diff --git a/src/Server/ServerMain/init.server.luau b/src/Server/ServerMain/init.server.luau index 1923ac7..b236511 100644 --- a/src/Server/ServerMain/init.server.luau +++ b/src/Server/ServerMain/init.server.luau @@ -92,7 +92,9 @@ local function OnPlayerAdded(Player: Player) -- 加载对应玩家的其他系统代理 Proxies.EquipmentProxy:InitPlayer(Player) Proxies.PlayerInfoProxy:InitPlayer(Player) + Proxies.MobsProxy:InitPlayer(Player) Proxies.LevelProxy:InitPlayer(Player) + Proxies.PlayerFightProxy:InitPlayer(Player) end local function OnPlayerRemoving(Player: Player) diff --git a/src/ServerStorage/Base/Behaviour.luau b/src/ServerStorage/Base/Behaviour.luau new file mode 100644 index 0000000..fa4d8e1 --- /dev/null +++ b/src/ServerStorage/Base/Behaviour.luau @@ -0,0 +1,83 @@ +local Behaviour = {} +Behaviour.__index = Behaviour + +--> Dependencies +local TypeList = require(script.Parent.TypeList) + +-------------------------------------------------------------------------------- + +-- 刷新时,重新载入,暂时不考虑性能 + +-- 初始化内容 +function Behaviour:Init(Character: TypeList.Character) + local self = {} + self.Character = Character + self.CheckData = nil + self.ExeTask = nil + + local Humanoid = self.Character.Humanoid + -- 监听属性变化 + self.ConAttribtueChanged = Humanoid.AttributeChanged:Connect(function(attributeKey: string, attributeValue: any) + -- 以后这里要是有其他状态也可以加打断 + if attributeKey == "Died" and attributeValue == true then + self:OnDied() + end + end) + return self +end + +-- 执行检查(主要重写部分) return 优先级, 执行数据 +function Behaviour:Check(CheckInfo: table) + -- 返回优先级,执行数据 + return -1, self.CheckData +end + +-- 具体执行内容(主要重写部分) +function Behaviour:Execute() + +end + +-- 检查当前状态是否可执行 +function Behaviour:CheckStat() + if not self.Character then warn("Behaviour Character not found") return false end + -- 死亡检查 + if self.Character:GetState("Died") then return false end + + -- 执行状态中检查 + local FightingFolder = self.Character:FindFirstChild("Fighting") + local ExecutingState = FightingFolder:FindFirstChild("ExecutingState") + -- 其他内容执行中,就false + if ExecutingState.Value == true then return false end + return true +end + +-- 改变当前执行状态标记 +function Behaviour:ChangeExecutingState(State: boolean) + if not self.Character then warn("Behaviour Character not found") return end + local FightingFolder = self.Character:FindFirstChild("Fighting") + local ExecutingState = FightingFolder:FindFirstChild("ExecutingState") + ExecutingState.Value = State +end + +-- 销毁 +function Behaviour:Destroy() + if self.ConAttribtueChanged then + self.ConAttribtueChanged:Disconnect() + self.ConAttribtueChanged = nil + end + if self.LoopTask then + task.cancel(self.LoopTask) + self.LoopTask = nil + end + self = nil +end + +-- 死亡时,主要打断当前执行状态 +function Behaviour:OnDied() + if self.ExeTask then + task.cancel(self.ExeTask) + self.ExeTask = nil + end +end + +return Behaviour \ No newline at end of file diff --git a/src/ServerStorage/Base/Character.luau b/src/ServerStorage/Base/Character.luau index 4a9297d..b8394df 100644 --- a/src/ServerStorage/Base/Character.luau +++ b/src/ServerStorage/Base/Character.luau @@ -35,12 +35,11 @@ function Character.new(Player: Player, CharacterModel: Model, CharacterData: tab self.Config["max" .. attributeKey] = attributeValue Attributes:SetAttribute("max" .. attributeKey, attributeValue) end - - local conAttribute = self.AttributeChanged:Connect(function(attributeKey: string, attributeValue: number) - self:ChangeAttribute(attributeKey, attributeValue) - end) - table.insert(self.Connections, conAttribute) end + local conAttribute = Attributes.AttributeChanged:Connect(function(attributeKey: string, attributeValue: number) + self:ChangeAttribute(attributeKey, attributeValue) + end) + table.insert(self.Connections, conAttribute) -- 配置角色状态数据 local statsData = { @@ -68,7 +67,7 @@ function Character.new(Player: Player, CharacterModel: Model, CharacterData: tab return self end -function Character:GetAttribute(attributeKey: string) +function Character:GetAttributeValue(attributeKey: string) return self.Config[attributeKey], self.Instance.Attributes:GetAttribute(attributeKey) end diff --git a/src/ServerStorage/Base/TypeList.luau b/src/ServerStorage/Base/TypeList.luau index 0bb7234..0f7a4ff 100644 --- a/src/ServerStorage/Base/TypeList.luau +++ b/src/ServerStorage/Base/TypeList.luau @@ -9,4 +9,15 @@ export type Character = { Stats: table, } +export type Behaviour = { + Character: Character, + CheckData: any, + ExeTask: TaskScheduler, + Init: (Character: Character) -> Behaviour, + Check: (CheckInfo: table) -> (number, any), + ExecutingBehaviour: () -> boolean, + Execute: () -> (), + Destroy: () -> (), +} + return {} \ No newline at end of file diff --git a/src/ServerStorage/Modules/ArmorLib/Morph.luau b/src/ServerStorage/Modules/ArmorLib/Morph.luau deleted file mode 100644 index 947c49a..0000000 --- a/src/ServerStorage/Modules/ArmorLib/Morph.luau +++ /dev/null @@ -1,119 +0,0 @@ ---[[ - Evercyan @ March 2023 - Morph - - Morph is a module used to house the functions for armor purely related to morphing (changing physical appearance) - This includes creating & welding the armor model to the character, as well as visual changes like hiding limbs or accessories. -]] - -local Morph = {} - -function Morph:UpdateLimbTransparency(Character: Model, Armor) - for _, Instance in Character:GetChildren() do - local Transparency = if Armor then Armor.Config.BodyPartsVisible[Instance.Name] and 0 or 1 else Instance:GetAttribute("OriginalTransparency") - if Instance:IsA("BasePart") and Transparency then - if not Instance:GetAttribute("OriginalTransparency") then - Instance:SetAttribute("OriginalTransparency", Instance.Transparency) - end - - Instance.Transparency = Transparency - end - end -end - -function Morph:UpdateAccessoriesTransparency(Character: Model, Armor) - for _, Instance in Character:GetChildren() do - if Instance:IsA("Accessory") then - - for _, Piece in Instance:GetDescendants() do - local Transparency = if Armor then Armor.Config.AccessoriesVisible and 0 or 1 else Piece:GetAttribute("OriginalTransparency") - if Piece:IsA("BasePart") and Transparency then - if not Piece:GetAttribute("OriginalTransparency") then - Piece:SetAttribute("OriginalTransparency", Piece.Transparency) - end - Piece.Transparency = Transparency - end - end - end - end -end - --- Welds an armor's limb (Model) to the character's body limb. -local function WeldArmorLimb(ArmorLimb: Model, BodyLimb: BasePart) - local Middle = ArmorLimb:FindFirstChild("Middle") - - -- Weld the entire armor limb together (to Middle) - for _, Piece: Instance in ArmorLimb:GetDescendants() do - if Piece:IsA("BasePart") then - Piece.Anchored = false - Piece.CanCollide = false - Piece.CanQuery = false - Piece.Massless = true - - if Piece.Name ~= "Middle" then - local Weld = Instance.new("Weld") - Weld.Name = Piece.Name .."/".. Middle.Name - Weld.Part0 = Middle - Weld.Part1 = Piece - Weld.C1 = Piece.CFrame:Inverse() * Middle.CFrame - Weld.Parent = Middle - end - end - end - - -- Weld the armor limb base (Middle) to the body limb - local Weld = Instance.new("Weld") - Weld.Name = BodyLimb.Name .."/".. Middle.Name - Weld.Part0 = BodyLimb - Weld.Part1 = Middle - Weld.Parent = Middle -end - -function Morph:ApplyOutfit(Player: Player, Armor) - local Character = Player.Character - if not Character then return end - - if Character:FindFirstChild("ArmorGroup") then - self:ClearOutfit(Player) - end - - -- Update limb & accessory transparency - self:UpdateLimbTransparency(Character, Armor) - self:UpdateAccessoriesTransparency(Character, Armor) - - -- Create armor group - local ArmorGroup = Instance.new("Folder") - ArmorGroup.Name = "ArmorGroup" - - for _, ArmorLimb in Armor.Instance:GetChildren() do - if not ArmorLimb:IsA("Model") then continue end - ArmorLimb = ArmorLimb:Clone() - - local Middle = ArmorLimb:FindFirstChild("Middle") - local BodyLimb = Character:FindFirstChild(ArmorLimb.Name) - - if Middle and BodyLimb then - WeldArmorLimb(ArmorLimb, BodyLimb) - ArmorLimb.Parent = ArmorGroup - elseif not Middle then - warn(("Armor limb %s/%s is missing a \"Middle\" BasePart!"):format(Armor.Name, ArmorLimb.Name)) - end - end - - ArmorGroup.Parent = Character -end - -function Morph:ClearOutfit(Player: Player) - local Character = Player.Character - if not Character then return end - - self:UpdateLimbTransparency(Character) - self:UpdateAccessoriesTransparency(Character) - - local ArmorGroup = Character:FindFirstChild("ArmorGroup") - if ArmorGroup then - ArmorGroup:Destroy() - end -end - -return Morph \ No newline at end of file diff --git a/src/ServerStorage/Modules/ArmorLib/init.luau b/src/ServerStorage/Modules/ArmorLib/init.luau deleted file mode 100644 index b082db7..0000000 --- a/src/ServerStorage/Modules/ArmorLib/init.luau +++ /dev/null @@ -1,165 +0,0 @@ --- --[[ --- Evercyan @ March 2023 --- ArmorLib - --- ArmorLib is an item library that houses code that can be ran on the server relating --- to Armor, such as ArmorLib:Give(Player, Armor (ContentLib.Armor[...])), as well --- as equipping & unequipping armor, which is primarily ran through remotes fired from the --- client's Inventory Gui. --- ]] - --- --> Services --- local ReplicatedStorage = game:GetService("ReplicatedStorage") --- local Players = game:GetService("Players") - --- --> References --- local PlayerData = ReplicatedStorage:WaitForChild("PlayerData") - --- --> Dependencies --- local ContentLibrary = require(ReplicatedStorage.Modules.ContentLibrary) --- local Morph = require(script.Morph) - --- --> Variables -local ArmorLib = {} - --- -------------------------------------------------------------------------------- - --- -- Adds the armor to the player's data --- function ArmorLib:Give(Player: Player, Armor) --- local pData = PlayerData:WaitForChild(Player.UserId, 5) - --- if pData then --- if not pData.Items.Armor:FindFirstChild(Armor.Name) then --- local ValueObject = Instance.new("BoolValue") --- ValueObject.Name = Armor.Name --- ValueObject.Parent = pData.Items.Armor --- end --- else --- warn(("pData for Player '%s' doesn't exist! Did they leave?"):format(Player.Name)) --- end --- end - --- function ArmorLib:Trash(Player: Player, Armor) --- local pData = PlayerData:WaitForChild(Player.UserId, 5) - --- if pData then --- if pData.Items.Armor:FindFirstChild(Armor.Name) then --- pData.Items.Armor[Armor.Name]:Destroy() --- end --- if pData.ActiveArmor.Value == Armor.Name then --- pData.ActiveArmor.Value = "" --- ArmorLib:UnequipArmor(Player) --- end --- else --- warn(("pData for Player '%s' doesn't exist! Did they leave?"):format(Player.Name)) --- end --- end - --- function ArmorLib:EquipArmor(Player: Player, Armor) --- local Character = Player.Character --- local Humanoid = Character and Character:FindFirstChild("Humanoid") --- local Attributes = Humanoid and Humanoid:WaitForChild("Attributes", 1) --- if not Attributes or Humanoid.Health <= 0 then return end - --- -- Humanoid changes --- Attributes.Health:SetAttribute("Armor", Armor.Config.Health) --- Attributes.WalkSpeed:SetAttribute("Armor", Armor.Config.WalkSpeed) --- Attributes.JumpPower:SetAttribute("Armor", Armor.Config.JumpPower) - --- -- Morph changes --- Morph:ApplyOutfit(Player, Armor) --- end - --- function ArmorLib:UnequipArmor(Player: Player) --- local Character = Player.Character --- local Humanoid = Character and Character:FindFirstChild("Humanoid") --- local Attributes = Humanoid and Humanoid:FindFirstChild("Attributes") --- if not Attributes then return end - --- -- Humanoid changes --- Attributes.Health:SetAttribute("Armor", nil) --- Attributes.WalkSpeed:SetAttribute("Armor", nil) --- Attributes.JumpPower:SetAttribute("Armor", nil) - --- -- Morph changes --- Morph:ClearOutfit(Player) --- end - --- ---- Remotes ------------------------------------------------------------------- - --- local ChangeCd = {} - --- ReplicatedStorage.Remotes.EquipArmor.OnServerInvoke = function(Player, ArmorName: string) --- if not ArmorName or typeof(ArmorName) ~= "string" then --- return --- end - --- local Armor = ContentLibrary.Armor[ArmorName] - --- if Armor and not ChangeCd[Player.UserId] then --- ChangeCd[Player.UserId] = true --- task.delay(0.25, function() --- ChangeCd[Player.UserId] = nil --- end) - --- ArmorLib:EquipArmor(Player, Armor) - --- local pData = PlayerData:FindFirstChild(Player.UserId) --- if pData and pData.Items.Armor[ArmorName] and Armor then --- pData.ActiveArmor.Value = ArmorName --- end --- end --- end - --- ReplicatedStorage.Remotes.UnequipArmor.OnServerInvoke = function(Player) --- ArmorLib:UnequipArmor(Player) - --- local pData = PlayerData:FindFirstChild(Player.UserId) --- if pData then --- pData.ActiveArmor.Value = "" --- end --- end - --- -------------------------------------------------------------------------------- - --- local function OnPlayerAdded(Player: Player) --- local pData = PlayerData:WaitForChild(Player.UserId) - --- local function OnCharacterAdded(Character) --- local ActiveArmor = pData:WaitForChild("ActiveArmor") --- local Armor = ActiveArmor.Value ~= "" and ContentLibrary.Armor[ActiveArmor.Value] - --- -- Update any incoming accessories (CharacterAppearanceLoaded is really broken lol) --- local Connection = Character.ChildAdded:Connect(function(Child) --- if Child:IsA("Accessory") then --- Morph:UpdateAccessoriesTransparency(Character, ActiveArmor.Value ~= "" and ContentLibrary.Armor[ActiveArmor.Value]) --- end --- end) --- Player.CharacterRemoving:Once(function() --- Connection:Disconnect() --- end) - --- if Armor then --- if Player:HasAppearanceLoaded() then --- ArmorLib:EquipArmor(Player, Armor) --- else --- Player.CharacterAppearanceLoaded:Once(function() --- ArmorLib:EquipArmor(Player, Armor) --- end) --- end --- end --- end - --- Player.CharacterAdded:Connect(OnCharacterAdded) --- if Player.Character then --- OnCharacterAdded(Player.Character) --- end --- end - --- for _, Player in Players:GetChildren() do --- task.defer(OnPlayerAdded, Player) --- end - --- Players.PlayerAdded:Connect(OnPlayerAdded) - -return ArmorLib \ No newline at end of file diff --git a/src/ServerStorage/Modules/Behaviours/Move.luau b/src/ServerStorage/Modules/Behaviours/Move.luau new file mode 100644 index 0000000..dbff437 --- /dev/null +++ b/src/ServerStorage/Modules/Behaviours/Move.luau @@ -0,0 +1,67 @@ +-- 移动行为 + +--> Services +local ServerStorage = game:GetService("ServerStorage") + +--> Dependencies +local TypeList = require(ServerStorage.Base.TypeList) +local Behaviour = require(ServerStorage.Base.Behaviour) +local MobsProxy = require(ServerStorage.Proxy.MobsProxy) + +-------------------------------------------------------------------------------- + +local Move = {} +Move.__index = Move + +function Move:Init(Character: TypeList.Character, Player: Player) + local self = Behaviour:Init(Character) + self.Player = Player + setmetatable(self, Move) + return self +end + +function Move:Check(CheckInfo: table) + if Behaviour.CheckStat(self) then return -1, self.CheckData end + + local PlayerMobs = MobsProxy:GetPlayerMobs(self.Player) + if not PlayerMobs then warn("PlayerMobs not found") return end + + local closestMob, minDistance = nil, math.huge + for _, Mob in PlayerMobs do + if Mob.Instance and Mob.Instance.PrimaryPart then + local dist = (Mob.Instance.PrimaryPart.Position - self.Character.Instance.PrimaryPart.Position).Magnitude + if dist < minDistance then + minDistance = dist + closestMob = Mob + end + end + end + + self.CheckData = {} + if closestMob then + self.CheckData["ClosestCharacter"] = closestMob + return 10, self.CheckData + end + + -- 返回优先级,执行数据 + return -1, self.CheckData +end + +function Move:Execute() + self.ExeTask = task.spawn(function () + self:ChangeExecutingState(true) + self.Character.Humanoid.WalkToPart.CFrame = self.CheckData["ClosestCharacter"].Instance.PrimaryPart.CFrame + task.wait(0.5) + self:ChangeExecutingState(false) + end) +end + +function Move:Destroy() + if self.ExeTask then + task.cancel(self.ExeTask) + self.ExeTask = nil + end + Behaviour.Destroy(self) +end + +return Move \ No newline at end of file diff --git a/src/ServerStorage/Modules/DamageLib.luau b/src/ServerStorage/Modules/DamageLib.luau deleted file mode 100644 index b63ef43..0000000 --- a/src/ServerStorage/Modules/DamageLib.luau +++ /dev/null @@ -1,103 +0,0 @@ ---[[ - Evercyan @ March 2023 - DamageLib - - DamageLib houses code relating to damage for weapons, and any additional sources of damage - you may add to your game. It is recommended to have all code for damage run through here for consistency. - - If you're looking to adjust crit multiplier, gamepass rewards (ex. x3 damage), etc, you can do this under DamageLib:DamageMob(). -]] - ---> Services -local CollectionService = game:GetService("CollectionService") -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local ServerStorage = game:GetService("ServerStorage") - ---> Dependencies -local Mobs = require(ServerStorage.Modules.MobLib.MobList) - ---> Variables -local DamageCd = {} -local Random = Random.new() - --------------------------------------------------------------------------------- - -local DamageLib = {} - -function DamageLib:TagMobForDamage(Player: Player, Mob, Damage: number) - if Mob.isDead then return end - - local PlayerTags = Mob.Instance:FindFirstChild("PlayerTags") - if not PlayerTags then - PlayerTags = Instance.new("Configuration") - PlayerTags.Name = "PlayerTags" - PlayerTags.Parent = Mob.Instance - end - - local ExistingTag = PlayerTags:GetAttribute(Player.UserId) - if ExistingTag then - PlayerTags:SetAttribute(Player.UserId, ExistingTag + Damage) - else - PlayerTags:SetAttribute(Player.UserId, Damage) - end -end - -function DamageLib:DamageMob(Player: Player, Mob): number? - if Mob.isDead then return end - - local pData = ReplicatedStorage.PlayerData:FindFirstChild(Player.UserId) - local Level = pData and pData:FindFirstChild("Stats") and pData.Stats:FindFirstChild("Level") - if not Level or (Level.Value < Mob.Config.Level[2]) then - return - end - - -- Make sure the equipped tool can be found, so we can safely grab the damage from it. - -- Never pass damage as a number through a remote, as the client can manipulate this data. - local Character = Player.Character - local Tool = Character and Character:FindFirstChildOfClass("Tool") - local ItemConfig = Tool and Tool:FindFirstChild("ItemConfig") and require(Tool.ItemConfig) - if not ItemConfig or not ItemConfig.Damage then return end - - -- Damage Cooldown - if DamageCd[Player.UserId] then return end - DamageCd[Player.UserId] = true - task.delay(ItemConfig.Cooldown - 0.03, function() -- We subtract just a tad so inconsistencies with timing on the client (ie. time to raycast) is less likely to stop a hit from going through - DamageCd[Player.UserId] = nil - end) - - -- Calculate damage - local Damage = typeof(ItemConfig.Damage) == "table" and Random:NextInteger(unpack(ItemConfig.Damage)) or ItemConfig.Damage - local isCrit = Random:NextInteger(1, 10) == 1 - - if isCrit then - Damage *= 2 - end - - self:TagMobForDamage(Player, Mob, Damage) - Mob.Enemy.Health = math.clamp(Mob.Enemy.Health - Damage, 0, Mob.Enemy.MaxHealth) - ReplicatedStorage.Remotes.PlayerDamagedMob:FireClient(Player, Mob.Instance, Damage) - - return Damage -end - -ReplicatedStorage.Remotes.DamageMob.OnServerInvoke = function(Player, MobInstance: Model) - if MobInstance and typeof(MobInstance) == "Instance" and CollectionService:HasTag(MobInstance, "Mob") then - local Mob = Mobs[MobInstance] - - if Mob then - -- Follow - local Enemy = Mob.Instance:FindFirstChild("Enemy") - if Enemy and (not Enemy.WalkToPart or not Enemy.WalkToPart:IsDescendantOf(Player.Character)) then - local HumanoidRootPart = Mob.Instance:FindFirstChild("HumanoidRootPart") - if HumanoidRootPart then - HumanoidRootPart.Anchored = false - HumanoidRootPart:SetNetworkOwner(Player) - Enemy:MoveTo(Player.Character:GetPivot().Position) - end - end - return DamageLib:DamageMob(Player, Mob) - end - end -end - -return DamageLib \ No newline at end of file diff --git a/src/ServerStorage/Modules/MobLib/AI.luau b/src/ServerStorage/Modules/MobLib/AI.luau deleted file mode 100644 index 991142c..0000000 --- a/src/ServerStorage/Modules/MobLib/AI.luau +++ /dev/null @@ -1,173 +0,0 @@ ---[[ - Evercyan @ March 2023 - AI - - This script handles the physical behavior of mobs, notably Following and Jumping. - - Mobs automatically follow any player within range, but once attacked, they will no longer follow the closest - player within range for the follow session, and will instead start attacking the player which recently attacked it. -]] - ---> Services -local Players = game:GetService("Players") - ---> Dependencies -local MobList = require(script.Parent.MobList) - ---> Variables -local ActiveMobs = {} -local AI = {} - ----- UTILITY FUNCTIONS --------------------------------------------------------- - -local RaycastParams = RaycastParams.new() -RaycastParams.FilterDescendantsInstances = { - workspace:WaitForChild("Mobs"), - workspace:WaitForChild("Characters") -} -RaycastParams.RespectCanCollide = true -RaycastParams.IgnoreWater = true - -local function GetTableLength(t): number - local n = 0 - for _ in t do - n += 1 - end - return n -end - --------------------------------------------------------------------------------- - --- Gets the closest player within range to the given mob. If the closest player is the player the mob is already following, distance is 2x. --- Note: If streaming is enabled, MoveTo may produce undesired results if the max distance is ever higher than streaming minimum, such as the enemy --- appearing to 'idle' if the current distance/magnitude is between the streaming minimum and the max follow distance (including 2x possibility) -function AI:GetClosestPlayer(Mob: MobList.Mob): (Player?, number?) - local Closest = {Player = nil, Magnitude = math.huge} - - local ActivePlayer -- We retain a reference to this, so they have to get further away from the mob to stop following, instead of the usual distance. - if Mob.Enemy.WalkToPart then - local Player = Players:GetPlayerFromCharacter(Mob.Enemy.WalkToPart.Parent) - if Player then - ActivePlayer = Player - end - end - - for _, Player in Players:GetPlayers() do - local Character = Player.Character - if not Character then continue end - - local Magnitude = (Character:GetPivot().Position - Mob.Instance:GetPivot().Position).Magnitude - local MaxDistance = (ActivePlayer == Player and (Mob.Config.FollowDistance or 32) * 2) or Mob.Config.FollowDistance or 32 - - if Magnitude <= MaxDistance and Magnitude < Closest.Magnitude then - Closest.Player = Player - Closest.Magnitude = Magnitude - end - end - - return Closest.Player, Closest.Player and Closest.Magnitude -end - --- Adds the mob to the ActiveMobs table. This table runs a few times per second and updates follow & jump code for active mobs. --- Active mobs are unanchored, and anchored back when they're not active (not within follow range of any player) -function AI:StartTracking(Mob) - if not Mob.Destroyed then - ActiveMobs[Mob.Instance] = Mob - end -end - --- Requests the mob to perform a jump if there's an obstacle in the way of it. -function AI:RequestJump(Mob) - if not Mob.Instance then - return - end - - local Root: BasePart = Mob.Root - local JumpHeight: number = (Mob.Enemy.JumpPower ^ 2) / (2 * workspace.Gravity) * .8 - local HipPoint: CFrame = Root.CFrame + (Root.CFrame.LookVector*(Root.Size.Z/2-0.2)) + (Root.CFrame.UpVector/-Root.Size.Y/2) - - local RaycastResult = workspace:Raycast( - HipPoint.Position, - HipPoint.LookVector * Mob.Enemy.WalkSpeed/4, - RaycastParams - ) - - if RaycastResult then - -- There is an obstacle, but they should be able to jump over it! - if RaycastResult.Instance ~= workspace.Terrain then - local PartHeight: number = (RaycastResult.Instance.Position + Vector3.new(0, RaycastResult.Instance.Size.Y/2, 0)).Y - if (HipPoint.Position.Y + JumpHeight) > PartHeight then - Mob.Enemy.Jump = true - end - else - Mob.Enemy.Jump = true - end - end -end - --------------------------------------------------------------------------------- - --- Code to begin tracking, evenly spread out -task.defer(function() - local iterationsPerSec = 4 - while true do - local Mobn = GetTableLength(MobList) - local Quota = Mobn / (60/iterationsPerSec) - - for _, Mob in MobList do - if not Mob.isDead and not Mob.Destroyed then - local Player = AI:GetClosestPlayer(Mob) - - if Player and not ActiveMobs[Mob.Instance] then - task.defer(AI.StartTracking, AI, Mob) - Quota -= 1 - if Quota < 1 then - Quota += Mobn / (60/iterationsPerSec) - task.wait() - end - end - end - end - task.wait() - end -end) - --- Code to continue tracking & jumping on Active Mobs -task.defer(function() - while true do - task.wait(1/4) - for MobInstance, Mob in ActiveMobs do - task.spawn(function() - if not Mob.isDead and not Mob.Destroyed then - local Player, m = AI:GetClosestPlayer(Mob) - local Enemy = Mob.Enemy - - -- Simulation - if Mob.Root.Anchored then - Mob.Root.Anchored = false - end - if not Mob.Root:GetNetworkOwner() then - Mob.Root:SetNetworkOwner(Player) - task.wait(0.05) -- Give physics more time so it doesn't appear as choppy - end - - -- Tracking - if Player and Enemy then - Enemy:MoveTo(Player.Character:GetPivot().Position, Player.Character.PrimaryPart) - else - ActiveMobs[MobInstance] = nil - Mob.Root:SetNetworkOwner() - Enemy:MoveTo(Mob.Origin.Position) - end - - -- Jumping - AI:RequestJump(Mob) - else - ActiveMobs[MobInstance] = nil - end - end) - end - end -end) - -return AI \ No newline at end of file diff --git a/src/ServerStorage/Modules/MobLib/MobList.luau b/src/ServerStorage/Modules/MobLib/MobList.luau deleted file mode 100644 index 9cf0100..0000000 --- a/src/ServerStorage/Modules/MobLib/MobList.luau +++ /dev/null @@ -1,10 +0,0 @@ -export type Mob = { - Instance: Model, - Config: any, - Root: BasePart, - Enemy: Humanoid, - Origin: CFrame, - Respawn: (Mob) -> () -} - -return {} \ No newline at end of file diff --git a/src/ServerStorage/Modules/MobLib/init.luau b/src/ServerStorage/Modules/MobLib/init.luau deleted file mode 100644 index 3b5d03a..0000000 --- a/src/ServerStorage/Modules/MobLib/init.luau +++ /dev/null @@ -1,250 +0,0 @@ ---[[ - Evercyan @ March 2023 - MobLib - - MobLib is where the server-sided code for mobs is ran. This manages all of the logic, excluding the - minor logic on the client, like disabling certain humanoid state types for optimization. -]] - ---> Services -local CollectionService = game:GetService("CollectionService") -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local ServerStorage = game:GetService("ServerStorage") -local Players = game:GetService("Players") - ---> References -local PlayerData = ReplicatedStorage:WaitForChild("PlayerData") - ---> Dependencies -local ContentLibrary = require(ReplicatedStorage.Modules.ContentLibrary) -local FormatNumber = require(ReplicatedStorage.Modules.FormatNumber) -local AI = require(script.AI) - ---> Variables -local Mobs = require(script.MobList) -- Dictionary which stores a reference to every Mob -local DamageCooldown = {} -local Random = Random.new() - --------------------------------------------------------------------------------- - -local MobLib = {} -- Mirror table with the Mob constructor function - --- local Mob = {} -- Syntax sugar for mob-related functions --- Mob.__index = Mob - --- local MobsFolder = workspace:FindFirstChild("Mobs") --- if not MobsFolder then --- MobsFolder = Instance.new("Folder") --- MobsFolder.Name = "Mobs" --- MobsFolder.Parent = workspace --- end - --- function MobLib.new(MobInstance: Model): Mobs.Mob --- local HumanoidRootPart = MobInstance:FindFirstChild("HumanoidRootPart") :: BasePart --- local Enemy = MobInstance:WaitForChild("Enemy") :: Humanoid --- local MobConfig = MobInstance:FindFirstChild("MobConfig") and require(MobInstance:FindFirstChild("MobConfig")) --- if not HumanoidRootPart or not Enemy or not MobConfig then --- print(HumanoidRootPart, Enemy, MobConfig) --- error(("MobLib.new: Passed mob '%s' is missing vital components."):format(MobInstance.Name)) --- end - --- local Mob = setmetatable({}, Mob) --- Mob.Instance = MobInstance --- Mob.Config = MobConfig --- Mob.Root = HumanoidRootPart --- Mob.Enemy = Enemy --- Mob.Origin = HumanoidRootPart:GetPivot() --- Mob._Copy = MobInstance:Clone() - --- -- Initialize --- Enemy.MaxHealth = MobConfig.Health --- Enemy.Health = MobConfig.Health --- Enemy.WalkSpeed = MobConfig.WalkSpeed --- Enemy.JumpPower = MobConfig.JumpPower --- HumanoidRootPart.Anchored = true --- MobInstance.Parent = MobsFolder - --- -- Set humanoid states (helps prevent falling down & useless calculations - you're unlikely to have an enemy climbing without pathfinding) --- for _, EnumName in {"FallingDown", "Seated", "Flying", "Swimming", "Climbing"} do --- local HumanoidStateType = Enum.HumanoidStateType[EnumName] --- Enemy:SetStateEnabled(HumanoidStateType, false) --- if Enemy:GetState() == HumanoidStateType then --- Enemy:ChangeState(Enum.HumanoidStateType.Running) --- end --- end - --- -- Damage --- local function OnTouched(BasePart) --- local Player = Players:GetPlayerFromCharacter(BasePart.Parent) --- local Humanoid = Player and Player.Character:FindFirstChild("Humanoid") - --- if Humanoid and Enemy.Health > 0 then --- if DamageCooldown[Player.UserId] then --- return --- end --- DamageCooldown[Player.UserId] = true --- task.delay(0.5, function() --- DamageCooldown[Player.UserId] = nil --- end) - --- Humanoid.Health = math.clamp(Humanoid.Health - MobConfig.Damage, 0, Humanoid.MaxHealth) --- ReplicatedStorage.Remotes.MobDamagedPlayer:FireClient(Player, MobInstance, MobConfig.Damage) --- end --- end - --- (MobInstance:WaitForChild("Hitbox") :: BasePart).Touched:Connect(OnTouched) - --- -- Respawn --- Enemy.Died:Once(function() --- if not Mob.isDead then --- Mob.isDead = true --- Mob:ActivateRagdoll() --- Mob:AwardDrops() --- task.wait(MobConfig.RespawnTime or 5) --- Mob:Respawn() --- end --- end) - --- -- Following has finished. Anchor assembly to optimize. --- Enemy.MoveToFinished:Connect(function() --- if not AI:GetClosestPlayer(Mob) and not Mob.isDead then --- HumanoidRootPart.Anchored = true --- else --- AI:StartTracking(Mob) --- end --- end) - - --- Mobs[MobInstance] = Mob - --- return Mob --- end - --- function Mob:Destroy() --- if not self.Destroyed then --- self.Destroyed = true - --- Mobs[self.Instance] = nil --- self.Instance:Destroy() - --- -- Remove instance references --- self.Instance = nil --- self.Root = nil --- self.Enemy = nil --- self._Copy = nil --- end --- end - --- function Mob:TakeDamage(Damage: number) --- local Enemy = self.Enemy --- if not self.isDead then --- Enemy.Health = math.clamp(Enemy.Health - Damage, 0, Enemy.MaxHealth) --- end --- end - --- function Mob:Respawn() --- if not self.Destroyed then --- local NewMob = self._Copy --- self:Destroy() --- NewMob.Parent = MobsFolder --- end --- end - --- function Mob:ActivateRagdoll() --- for _, Item in self.Instance:GetDescendants() do --- if Item:IsA("Motor6D") then --- local Attachment0 = Instance.new("Attachment") --- Attachment0.CFrame = Item.C0 --- Attachment0.Parent = Item.Part0 - --- local Attachment1 = Instance.new("Attachment") --- Attachment1.CFrame = Item.C1 --- Attachment1.Parent = Item.Part1 - --- local Constraint = Instance.new("BallSocketConstraint") --- Constraint.Attachment0 = Attachment0 --- Constraint.Attachment1 = Attachment1 --- Constraint.LimitsEnabled = true --- Constraint.TwistLimitsEnabled = true --- Constraint.Parent = Item.Parent - --- Item.Enabled = false --- end --- end --- end - --- function Mob:AwardDrops() --- local PlayerTags = self.Instance:FindFirstChild("PlayerTags") :: Configuration --- if not PlayerTags then return end - --- for UserId, Damage: number in PlayerTags:GetAttributes() do --- UserId = tonumber(UserId) - --- local Player = Players:GetPlayerByUserId(UserId) --- if not Player then continue end - --- local Percent = Damage / self.Enemy.MaxHealth - --- if Percent >= 0.25 then --- local pData = PlayerData:FindFirstChild(Player.UserId) --- local Statistics = pData:FindFirstChild("Stats") --- local Items = pData:FindFirstChild("Items") - --- -- Stats --- if Statistics then --- for _, StatInfo in self.Config.Drops.Statistics do --- local StatName: string = StatInfo[1] --- local StatCount: number = StatInfo[2] - --- local Stat = Statistics:FindFirstChild(StatName) --- if Stat then --- Stat.Value += StatCount --- end --- end --- end - --- -- Items --- if Items then --- for _, ItemInfo in self.Config.Drops.Items do --- local ItemType: string = ItemInfo[1] --- local ItemName: string = ItemInfo[2] --- local DropChance: number = ItemInfo[3] - --- local Item = ContentLibrary[ItemType] and ContentLibrary[ItemType][ItemName] --- if Item then --- local isLucky = Random:NextInteger(1, DropChance) == 1 --- if isLucky then --- require(ServerStorage.Modules[ItemType .."Lib"]):Give(Player, Item) --- ReplicatedStorage.Remotes.SendNotification:FireClient(Player, --- "Item Dropped", --- self.Config.Name .." dropped ".. Item.Name .." at a 1/".. FormatNumber(DropChance, "Suffix") .." chance.", --- Item.Config.IconId --- ) --- end --- else --- warn("[Kit/MobLib/AwardDrops]: Item doesn't exist: '".. ItemType .."/".. ItemName ..".") --- end --- end --- end - --- -- TeleportLocation (TP) --- local TP = self.Config.TeleportLocation and workspace.TP:FindFirstChild(self.Config.TeleportLocation) --- local Character = Player.Character --- if TP and Character then --- Character:PivotTo(TP.CFrame + Vector3.yAxis*4) --- end --- end --- end --- end - --- CollectionService:GetInstanceAddedSignal("Mob"):Connect(function(MobInstance) --- if MobInstance:IsDescendantOf(workspace) then -- For some reason, HD Admin saves a copy of the map under ServerStorage (if you happen to use that), and the MobLib will attempt to clone its copy into workspace.Mobs. --- MobLib.new(MobInstance) --- end --- end) - --- for _, MobInstance in CollectionService:GetTagged("Mob") do --- task.defer(MobLib.new, MobInstance) --- end - -return MobLib \ No newline at end of file diff --git a/src/ServerStorage/Modules/ToolLib.luau b/src/ServerStorage/Modules/ToolLib.luau deleted file mode 100644 index b100195..0000000 --- a/src/ServerStorage/Modules/ToolLib.luau +++ /dev/null @@ -1,72 +0,0 @@ ---[[ - Evercyan @ March 2023 - ToolLib - - ToolLib is an item library that houses code that can be ran on the server relating - to Tools, such as ToolLib:Give(Player, Tool (ContentLib.Tool[...])) -]] - ---> Services -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local Players = game:GetService("Players") - ---> References -local PlayerData = ReplicatedStorage:WaitForChild("PlayerData") - ---> Dependencies -local ContentLibrary = require(ReplicatedStorage.Modules.ContentLibrary) - ---> Variables -local ToolLib = {} - --------------------------------------------------------------------------------- - --- Adds the tool to the player's data, as well as the ToolInstance under StarterGear & Backpack. -function ToolLib:Give(Player: Player, Tool) - local pData = PlayerData:WaitForChild(Player.UserId, 5) - - if pData then - if not pData.Items.Tool:FindFirstChild(Tool.Name) then - local ValueObject = Instance.new("BoolValue") - ValueObject.Name = Tool.Name - ValueObject.Parent = pData.Items.Tool - - local StarterGear = Player:FindFirstChild("StarterGear") - local Backpack = Player:WaitForChild("Backpack") - - if StarterGear and not StarterGear:FindFirstChild(Tool.Name) then - Tool.Instance:Clone().Parent = StarterGear - end - - if Backpack and not Backpack:FindFirstChild(Tool.Name) then - Tool.Instance:Clone().Parent = Backpack - end - end - else - warn(("pData for Player '%s' doesn't exist! Did they leave?"):format(Player.Name)) - end -end - -function ToolLib:Trash(Player: Player, Tool) - local pData = PlayerData:WaitForChild(Player.UserId, 5) - - if pData then - if pData.Items.Tool:FindFirstChild(Tool.Name) then - pData.Items.Tool[Tool.Name]:Destroy() - end - - if Player.StarterGear:FindFirstChild(Tool.Name) then - Player.StarterGear[Tool.Name]:Destroy() - end - if Player.Backpack:FindFirstChild(Tool.Name) then - Player.Backpack[Tool.Name]:Destroy() - end - if Player.Character and Player.Character:FindFirstChild(Tool.Name) then - Player.Character[Tool.Name]:Destroy() - end - else - warn(("pData for Player '%s' doesn't exist! Did they leave?"):format(Player.Name)) - end -end - -return ToolLib \ No newline at end of file diff --git a/src/ServerStorage/Proxy/DamageProxy.luau b/src/ServerStorage/Proxy/DamageProxy.luau index d3a1ece..c0669ac 100644 --- a/src/ServerStorage/Proxy/DamageProxy.luau +++ b/src/ServerStorage/Proxy/DamageProxy.luau @@ -19,13 +19,15 @@ local DamageTag = { export type DamageTag = "Normal" | "Critical" export type DamageType = "Normal" | "Skill" - export type DamageInfo = { Damage: number, Type: DamageType, Tag: DamageTag, } +DamageProxy.DamageType = DamageType +DamageProxy.DamageTag = DamageTag + -------------------------------------------------------------------------------- function DamageProxy:GetHumanoid(Target: Model) @@ -43,7 +45,7 @@ end function DamageProxy:IsDied(Target: Model) local Humanoid = self:GetHumanoid(Target) - return Humanoid:GetAttribute("Health") <= 0 + return Humanoid:GetAttribute("hp") <= 0 end function DamageProxy:TakeDamage(Caster: TypeList.Character, Victim: TypeList.Character, DamageInfos: {DamageInfo}) @@ -53,8 +55,8 @@ function DamageProxy:TakeDamage(Caster: TypeList.Character, Victim: TypeList.Cha local DamageTag = DamageInfo.DamageTag -- 伤害计算 - local VictimHealth = Victim:GetAttribute("health") - Victim:ChangeAttribute("health", math.max(0, VictimHealth - Damage)) + local VictimHealth = Victim:GetAttributeValue("hp") + Victim:ChangeAttribute("hp", math.max(0, VictimHealth - Damage)) end end diff --git a/src/ServerStorage/Proxy/LevelProxy.luau b/src/ServerStorage/Proxy/LevelProxy.luau index d261ed2..78bb243 100644 --- a/src/ServerStorage/Proxy/LevelProxy.luau +++ b/src/ServerStorage/Proxy/LevelProxy.luau @@ -19,6 +19,7 @@ local JsonLevel = require(ReplicatedStorage.Json.Level) local STORE_NAME = "Level" local ENUM_LEVEL_TYPE = { Main = 1, + BossFail = 0, } -------------------------------------------------------------------------------- @@ -83,7 +84,6 @@ end local function OnMobDied(Player: Player, Mob: TypeList.Character) for _, mob in LevelProxy.pData[Player.UserId].Mobs do if mob ~= Mob then continue end - table.remove(LevelProxy.pData[Player.UserId].Mobs, mob) -- 怪物清除判断 @@ -112,9 +112,9 @@ function LevelProxy:InitPlayer(Player: Player) local PlayerLevelFolder = Utils:CreateFolder(Player.UserId, LevelFolder) local ProgressFolder = Utils:CreateFolder("Progress", PlayerLevelFolder) local DungeonFolder = Utils:CreateFolder("Dungeon", PlayerLevelFolder) + -- 当前关卡状态 不存储 local ChallengeFolder = Utils:CreateFolder("Challenge", PlayerLevelFolder) - -- 当前关卡状态 - Utils:CreateFolder("Stats", PlayerLevelFolder) + -- Utils:CreateFolder("Stats", PlayerLevelFolder) -- 新玩家数据初始化 if not ArchiveProxy.pData[Player.UserId][STORE_NAME] then @@ -150,7 +150,7 @@ function LevelProxy:InitPlayer(Player: Player) CreateLevelInstance(Player, ChallengeFolder, key, value) end - self:ChallengeLevel(Player, 1) + -- self:ChallengeLevel(Player, 1) end -- 挑战关卡(挑战副本用另一个函数) @@ -166,7 +166,7 @@ function LevelProxy:ChallengeLevel(Player: Player, LevelId: number) local ChallengeFolder = LevelFolder:FindFirstChild("Challenge") if not ChallengeFolder then warn("ChallengeFolder not found") return end local levelTask = task.defer(function() - ChangeValue(Player, ChallengeFolder, "IsBoss", LevelData.type == 1 and true or false) + ChangeValue(Player, ChallengeFolder, "IsBoss", LevelData.type == 2 and true or false) ChangeValue(Player, ChallengeFolder, "LevelId", LevelId) ChangeValue(Player, ChallengeFolder, "Time", 0) ChangeValue(Player, ChallengeFolder, "NowWave", 0) @@ -185,7 +185,6 @@ function LevelProxy:ChallengeLevel(Player: Player, LevelId: number) ChangeValue(Player, ChallengeFolder, "NowWave", LevelProxy.pData[Player.UserId].NowWave + 1) ChangeValue(Player, ChallengeFolder, "SpawnWaveFinish", true) - ChangeValue(Player, ChallengeFolder, "NowWave", LevelProxy.pData[Player.UserId].NowWave + 1) local waveData = LevelData.wave[LevelProxy.pData[Player.UserId].NowWave] for i = 1, #waveData, 3 do local mobId = waveData[i + 1] @@ -193,13 +192,26 @@ function LevelProxy:ChallengeLevel(Player: Player, LevelId: number) for _ = 1, mobCount do local mob = MobsProxy:CreateMob(Player, mobId, LevelData.atkBonus, LevelData.hpBonus, OnMobDied) table.insert(LevelProxy.pData[Player.UserId].Mobs, mob) - print(mob) end end ChangeValue(Player, ChallengeFolder, "SpawnWaveFinish", true) end + + if LevelProxy.pData[Player.UserId].NowWave >= LevelProxy.pData[Player.UserId].ShouldWave and + LevelProxy.pData[Player.UserId].SpawnWaveFinish == true and + #LevelProxy.pData[Player.UserId].Mobs == 0 then + local maxWave = #LevelData.wave + if LevelProxy.pData[Player.UserId].NowWave < maxWave then + -- 下一波 + ChangeValue(Player, ChallengeFolder, "ShouldWave", LevelProxy.pData[Player.UserId].ShouldWave + 1) + else + -- 挑战胜利 + self:ChallengeEnd(Player, true) + end + end + + -- 时间结束 if LevelData.timeLimit then - -- 时间结束 if LevelProxy.pData[Player.UserId].Time >= LevelData.timeLimit then self:ChallengeEnd(Player, false) end @@ -215,14 +227,17 @@ function LevelProxy:ChallengeEnd(Player: Player, result: boolean) local LevelFolder = Utils:CreateFolder(STORE_NAME, pData) local ProgressFolder = Utils:CreateFolder("Progress", LevelFolder) -- 清除剩余怪物 - for _, mob in LevelProxy.pData[Player.UserId].Mobs do - mob:Died() - end + for _, mob in LevelProxy.pData[Player.UserId].Mobs do mob:Died() end LevelProxy.pData[Player.UserId].Mobs = {} -- 判断玩家是否通关 if result then ChangeValue(Player, ProgressFolder, "LevelId", LevelProxy.pData[Player.UserId].LevelId + 1) + else + local IsBoss = LevelProxy.pData[Player.UserId].IsBoss + if IsBoss then + ChangeValue(Player, ProgressFolder, "BossFail", LevelProxy.pData[Player.UserId].LevelId) + end end end diff --git a/src/ServerStorage/Proxy/MobsProxy/AI.luau b/src/ServerStorage/Proxy/MobsProxy/AI.luau index bdbc1ba..92b946f 100644 --- a/src/ServerStorage/Proxy/MobsProxy/AI.luau +++ b/src/ServerStorage/Proxy/MobsProxy/AI.luau @@ -1,12 +1,4 @@ ---[[ - Evercyan @ March 2023 - AI - - This script handles the physical behavior of mobs, notably Following and Jumping. - - Mobs automatically follow any player within range, but once attacked, they will no longer follow the closest - player within range for the follow session, and will instead start attacking the player which recently attacked it. -]] +-- 怪物ai 通用维护版 --> Services local Players = game:GetService("Players") @@ -23,7 +15,7 @@ local AI = {} -------------------------------------------------------------------------------- -- 获取两个单位之间的距离 -function AI:GetModelDistance(Unit1: TypeList.Character, Unit2: TypeList.Character): number +function AI:GetModelDistance(Unit1: Model, Unit2: Model): number return (Unit1:GetPivot().Position - Unit2:GetPivot().Position).Magnitude end diff --git a/src/ServerStorage/Proxy/MobsProxy/init.luau b/src/ServerStorage/Proxy/MobsProxy/init.luau index 650f15a..4e23130 100644 --- a/src/ServerStorage/Proxy/MobsProxy/init.luau +++ b/src/ServerStorage/Proxy/MobsProxy/init.luau @@ -28,10 +28,8 @@ local MobsFolder = Utils:CreateFolder("Mobs", game.Workspace) -------------------------------------------------------------------------------- local function GetPlayerMobsFolder(Player: Player) - local pData = Utils:GetPlayerDataFolder(Player) - if not pData then return end - local MobsFolder = pData:FindFirstChild("Mobs"):FindFirstChild(Player.UserId) - return MobsFolder + if not Player then warn("GetPlayerMobsFolder Player not found", Player.Name) return end + return MobsFolder:FindFirstChild(Player.UserId) end local function FindMobPrefab(MobModelName: string) @@ -41,7 +39,7 @@ end -------------------------------------------------------------------------------- local Mob = {} -Mob.__index = Mob +Mob.__index = Character function Mob.new(Player: Player, MobId: number, OnMobDied: ((Player: Player, Mob: TypeList.Character) -> ())?) -- 获取玩家怪物目录 @@ -49,12 +47,12 @@ function Mob.new(Player: Player, MobId: number, OnMobDied: ((Player: Player, Mob if not playerMobsFolder then return end -- 获取怪物数据 - local MobData = Utils:GetJsonData(JsonMob, MobId) + local MobData = Utils:GetIdDataFromJson(JsonMob, MobId) if not MobData then warn("Mob Data not found", MobId) return end -- 获取怪物模型 - local MobInstance = FindMobPrefab(MobData.Model) - if not MobInstance then warn("Mob Prefab not found", MobData.Model) return end + local MobInstance = FindMobPrefab(MobData.model) + if not MobInstance then warn("Mob Prefab not found", MobData.model) return end -- 克隆怪物模型 local newMobModel = MobInstance:Clone() @@ -69,14 +67,14 @@ function Mob.new(Player: Player, MobId: number, OnMobDied: ((Player: Player, Mob -- 死亡函数 if OnMobDied then Mob.OnDied = OnMobDied end - -- 接入统一AI - self.Humanoid.MoveToFinished:Connect(function() - if not AI:GetClosestPlayer(Mob) and not Mob.isDead then - self.Root.Anchored = true - else - AI:StartTracking(Mob) - end - end) + -- -- 接入统一AI + -- self.Humanoid.MoveToFinished:Connect(function() + -- if not AI:GetClosestPlayer(Mob) and not Mob.isDead then + -- self.Root.Anchored = true + -- else + -- AI:StartTracking(Mob) + -- end + -- end) return self end @@ -91,9 +89,10 @@ end -- 给玩家创建怪物 function MobsProxy:CreateMob(Player: Player, MobId: number, AtkBonus: number?, HpBonus: number?, OnMobDied: ((Player: Player, Mob: TypeList.Character) -> ())?) local Mob = Mob.new(Player, MobId, OnMobDied) + AI:StartTracking(Mob) -- 关卡系数 - if AtkBonus then Mob:ChangeValue("attack", math.floor(Mob.attack * (AtkBonus / 1000))) end - if HpBonus then Mob:ChangeValue("hp", math.floor(Mob.hp * (HpBonus / 1000))) end + if AtkBonus then Mob:ChangeAttribute("attack", math.floor(Mob.Config.attack * (AtkBonus / 1000))) end + if HpBonus then Mob:ChangeAttribute("hp", math.floor(Mob.Config.hp * (HpBonus / 1000))) end MobsProxy.pData[Player.UserId][Mob.Instance] = Mob return Mob end @@ -118,12 +117,16 @@ end function MobsProxy:InitPlayer(Player: Player) local pData = Utils:GetPlayerDataFolder(Player) - if not pData then return end + if not pData then warn("Player Data not found", Player.Name) return end Utils:CreateFolder(Player.UserId, MobsFolder) MobsProxy.pData[Player.UserId] = {} end +function MobsProxy:GetPlayerMobs(Player: Player) + return MobsProxy.pData[Player.UserId] +end + function MobsProxy:OnPlayerRemoving(Player: Player) local pData = Utils:GetPlayerDataFolder(Player) if not pData then return end diff --git a/src/ServerStorage/Proxy/PlayerFightProxy/LevelLoop.luau b/src/ServerStorage/Proxy/PlayerFightProxy/LevelLoop.luau new file mode 100644 index 0000000..2542b06 --- /dev/null +++ b/src/ServerStorage/Proxy/PlayerFightProxy/LevelLoop.luau @@ -0,0 +1,73 @@ +-- 玩家关卡循环 + +--> Services +local ServerStorage = game:GetService("ServerStorage") +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +--> Events +local BD_ChallengeEnd = ReplicatedStorage.Events.BD_ChallengeEnd + +--> Dependencies +local LevelProxy = require(ServerStorage.Proxy.LevelProxy) +local TypeList = require(ServerStorage.Base.TypeList) +local Utils = require(ReplicatedStorage.Tools.Utils) + +--> Json +local JsonLevel = require(ReplicatedStorage.Json.Level) + +-------------------------------------------------------------------------------- + +local LevelLoop = {} +LevelLoop.__index = LevelLoop + +function LevelLoop.new(Player: Player, Character: TypeList.Character) + print("LevelLoop:Init") + local self = {} + setmetatable(self, LevelLoop) + self.Player = Player + self.Character = Character + self.TaskAutoChallenge = nil + + self.ConChallengeEnd = BD_ChallengeEnd.Event:Connect(function(Player: Player, LevelId: number) + if Player ~= self.Player then return end + self:OnChallengeEnd(Player, LevelId) + end) + + self:AutoChallenge() + return self +end + +function LevelLoop:AutoChallenge() + print("AutoChallenge") + local LevelFolder = game.Workspace:WaitForChild("Level"):WaitForChild(self.Player.UserId) + local ProgressFolder = LevelFolder:FindFirstChild("Progress") + + local LevelId = ProgressFolder:FindFirstChild("Main").Value + local FailBossId = ProgressFolder:FindFirstChild("BossFail").Value + -- Boss失败了就去挑战上一个关卡 + if FailBossId == LevelId then + LevelId = LevelId - 1 + end + LevelProxy:ChallengeLevel(self.Player, LevelId) +end + +function LevelLoop:OnChallengeEnd(Player: Player, LevelId: number) + self.TaskAutoChallenge = task.defer(function() + task.wait(3) + self:AutoChallenge() + end) +end + +function LevelLoop:Destroy() + if self.TaskAutoChallenge then + task.cancel(self.TaskAutoChallenge) + self.TaskAutoChallenge = nil + end + if self.ConChallengeEnd then + self.ConChallengeEnd:Disconnect() + self.ConChallengeEnd = nil + end + self = nil +end + +return LevelLoop \ No newline at end of file diff --git a/src/ServerStorage/Proxy/PlayerFightProxy/PlayerAI.luau b/src/ServerStorage/Proxy/PlayerFightProxy/PlayerAI.luau new file mode 100644 index 0000000..02cb310 --- /dev/null +++ b/src/ServerStorage/Proxy/PlayerFightProxy/PlayerAI.luau @@ -0,0 +1,101 @@ +-- 玩家AI +local PlayerAI = {} +PlayerAI.__index = PlayerAI + +--> Services +local Players = game:GetService("Players") +local ServerStorage = game:GetService("ServerStorage") + +--> Dependencies +local TypeList = require(ServerStorage.Base.TypeList) + +--> Variables +local DamageProxy = require(ServerStorage.Proxy.DamageProxy) +local ActivePlayers = {} + +local BehavioursFolder = ServerStorage:FindFirstChild("Modules"):FindFirstChild("Behaviours") + +local Behaviours = {} +local BehaviourFolder = ServerStorage:FindFirstChild("Modules"):FindFirstChild("Behaviours") +if BehaviourFolder then + for _, behaviourModule in ipairs(BehaviourFolder:GetChildren()) do + if behaviourModule:IsA("ModuleScript") then + local success, result = pcall(require, behaviourModule) + if success then + -- 去掉文件名后缀 + local name = behaviourModule.Name + Behaviours[name] = result + else + warn("加载代理模块失败: " .. behaviourModule.Name, result) + end + end + end +end + + +-------------------------------------------------------------------------------- + +-- 新建玩家AI +function PlayerAI.new(Player: Player, PlayerRole: TypeList.Character) + local self = {} + setmetatable(self, PlayerAI) + self.Character = PlayerRole + self.Player = Player + + self.BehaviourList = {} + self.LoopTask = task.spawn(function() + while task.wait(0.25) do + self:Update() + end + end) + + return self +end + +-- 核心循环 +function PlayerAI:Update() + local maxPriority = -math.huge + local bestBehaviour = nil + local bestCheckData = nil + + for _, behaviour in self.BehaviourList do + local priority, checkData = behaviour:Check(self.Character) + if priority > maxPriority then + maxPriority = priority + bestBehaviour = behaviour + bestCheckData = checkData + end + end + + if bestBehaviour and maxPriority > 0 then + bestBehaviour:Execute(bestCheckData) + end +end + +-- 动态添加行为 +function PlayerAI:AddBehaviour(BehaviourName: string) + if not Behaviours[BehaviourName] then warn("Behaviour not found") return end + local newBehaviour = Behaviours[BehaviourName]:Init(self.Character, self.Player) + self.BehaviourList[BehaviourName] = newBehaviour +end + +-- 动态删除行为 +function PlayerAI:RemoveBehaviour(BehaviourName: string) + if self.BehaviourList[BehaviourName] then + self.BehaviourList[BehaviourName]:Destroy() + self.BehaviourList[BehaviourName] = nil + end +end + +-- 销毁AI +function PlayerAI:Destroy() + for _, behaviour in self.BehaviourList do behaviour:Destroy() end + self.BehaviourList = nil + if self.LoopTask then + task.cancel(self.LoopTask) + self.LoopTask = nil + end + self = nil +end + +return PlayerAI \ No newline at end of file diff --git a/src/ServerStorage/Proxy/PlayerFightProxy/init.luau b/src/ServerStorage/Proxy/PlayerFightProxy/init.luau new file mode 100644 index 0000000..15c679d --- /dev/null +++ b/src/ServerStorage/Proxy/PlayerFightProxy/init.luau @@ -0,0 +1,104 @@ +-- 玩家循环代理 +local PlayerFightProxy = {} + +--> Services +local Players = game:GetService("Players") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local ServerStorage = game:GetService("ServerStorage") + +--> Variables +local Character = require(ServerStorage.Base.Character) +local Utils = require(ReplicatedStorage.Tools.Utils) +local LevelProxy = require(ServerStorage.Proxy.LevelProxy) + +--> Json +local JsonCharacter = require(ReplicatedStorage.Json.Character) + +--> Dependencies +local LevelLoop = require(script.LevelLoop) +local PlayerAI = require(script.PlayerAI) + +-------------------------------------------------------------------------------- + +local function WaitForCharacter(Player: Player) + while not Player.Character or not Player.Character.Parent do + Player.CharacterAdded:Wait() + end + return Player.Character +end + +-------------------------------------------------------------------------------- + +local PlayerRole = {} +PlayerRole.__index = Character + +function PlayerRole.new(Player: Player, CharacterId: number) + local playerCharacter = WaitForCharacter(Player) + + -- 获取怪物数据 + local CharacterData = Utils:GetIdDataFromJson(JsonCharacter, CharacterId) + if not CharacterData then warn("CharacterId Data not found", CharacterId) return end + + -- 调用父类Character的new方法,初始化通用属性 + local self = Character.new(Player, playerCharacter, CharacterData) + setmetatable(self, PlayerRole) + return self +end + +function PlayerRole:Died() + self:ChangeState("Died", true) + LevelProxy:ChallengeEnd(self.Player, false) +end + +function PlayerRole:Respawn() + self:ChangeState("Died", false) + self:ChangeAttribute("hp", self.Config.maxhp) + -- 重置玩家位置 + +end + +-------------------------------------------------------------------------------- + +function PlayerFightProxy:InitPlayer(Player: Player) + if not PlayerFightProxy.pData then PlayerFightProxy.pData = {} end + if not PlayerFightProxy.pData[Player.UserId] then + PlayerFightProxy.pData[Player.UserId] = {} + end + + -- 生成玩家状态 + local PlayerRole = PlayerRole.new(Player, 1) + PlayerFightProxy.pData[Player.UserId].PlayerRole = PlayerRole + + -- 生成关卡循环 + local LevelLoop = LevelLoop.new(Player, PlayerRole) + PlayerFightProxy.pData[Player.UserId].LevelLoop = LevelLoop + + -- 生成玩家AI + local PlayerAI = PlayerAI.new(Player, PlayerRole) + PlayerFightProxy.pData[Player.UserId].PlayerAI = PlayerAI + PlayerAI:AddBehaviour("Move") +end + + +-- 重置玩家状态,但是不删除 +function PlayerFightProxy:CleanPlayer(Player: Player) + -- 玩家角色重生 + PlayerFightProxy.pData[Player.UserId].PlayerRole:Respawn() + -- AI重新生成 + PlayerFightProxy.pData[Player.UserId].PlayerAI:Destroy() + PlayerFightProxy.pData[Player.UserId].PlayerAI = nil + + local PlayerAI = PlayerAI.new(PlayerRole) + PlayerFightProxy.pData[Player.UserId].PlayerAI = PlayerAI +end + +function PlayerFightProxy:OnPlayerRemoving(Player: Player) + if not PlayerFightProxy.pData[Player.UserId] then warn("PlayerFight Remove Data not found", Player.Name) return end + PlayerFightProxy.pData[Player.UserId] = nil +end + +Players.PlayerRemoving:Connect(function(Player: Player) + PlayerFightProxy:OnPlayerRemoving(Player) +end) + +return PlayerFightProxy \ No newline at end of file diff --git a/src/ServerStorage/Proxy/PlayerInfoProxy.luau b/src/ServerStorage/Proxy/PlayerInfoProxy.luau index b949cf0..0489abe 100644 --- a/src/ServerStorage/Proxy/PlayerInfoProxy.luau +++ b/src/ServerStorage/Proxy/PlayerInfoProxy.luau @@ -1,4 +1,4 @@ --- 玩家基础信息代理 +-- 玩家通用信息 local PlayerInfoProxy = {} --> Services diff --git a/src/ServerStorage/Proxy/PlayerLoopProxy/PlayerAI.luau b/src/ServerStorage/Proxy/PlayerLoopProxy/PlayerAI.luau deleted file mode 100644 index e69de29..0000000 diff --git a/src/ServerStorage/Proxy/PlayerLoopProxy/init.luau b/src/ServerStorage/Proxy/PlayerLoopProxy/init.luau deleted file mode 100644 index a03e5cd..0000000 --- a/src/ServerStorage/Proxy/PlayerLoopProxy/init.luau +++ /dev/null @@ -1,6 +0,0 @@ --- 玩家循环代理 - -local PlayerLoopProxy = {} - - -return PlayerLoopProxy \ No newline at end of file