diff --git a/excel/enemy.xlsx b/excel/enemy.xlsx index 212ea3b..bc9b0ad 100644 Binary files a/excel/enemy.xlsx and b/excel/enemy.xlsx differ diff --git a/src/ReplicatedStorage/Json/Enemy.json b/src/ReplicatedStorage/Json/Enemy.json index ce4818a..cd2d6ad 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,"model":"Thief"}, -{"id":2,"type":1,"name":2,"atk":30,"hp":300,"model":"Thief"}, -{"id":1000,"type":2,"name":1000,"atk":50,"hp":1000,"model":"Thief"} +{"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"} ] \ No newline at end of file diff --git a/src/ServerStorage/Proxy/ArchiveProxy.luau b/src/ServerStorage/Proxy/ArchiveProxy.luau index 7ca0269..3b170d0 100644 --- a/src/ServerStorage/Proxy/ArchiveProxy.luau +++ b/src/ServerStorage/Proxy/ArchiveProxy.luau @@ -1,8 +1,6 @@ -- 数据存储代理 local ArchiveProxy = {} -print("进入") - --> Services local CollectionService = game:GetService("CollectionService") local ReplicatedStorage = game:GetService("ReplicatedStorage") diff --git a/src/ServerStorage/Proxy/DamageProxy.luau b/src/ServerStorage/Proxy/DamageProxy.luau new file mode 100644 index 0000000..834f0fa --- /dev/null +++ b/src/ServerStorage/Proxy/DamageProxy.luau @@ -0,0 +1,36 @@ +-- 伤害代理 +local DamageProxy = {} + +-- 伤害类型枚举 +local DamageType = { + NORMAL = "Normal", -- 普攻 + SKILL = "Skill", -- 技能 +} +local DamageTag = { + NORMAL = "Normal", -- 普攻 + CRIT = "Crit", -- 暴击 +} + +export type DamageTag = "Normal" | "Critical" +export type DamageType = "Normal" | "Skill" + + +export type DamageInfo = { + Damage: number, + Type: DamageType, + Tag: DamageTag, +} + +function DamageProxy:TakeDamage(Caster: Model, Victim: Model, DamageInfos: {DamageInfo}) + for _, DamageInfo in DamageInfos do + local Damage = DamageInfo.Damage + local DamageType = DamageInfo.DamageType + local DamageTag = DamageInfo.DamageTag + + -- 伤害计算 + end + +end + + +return DamageProxy \ No newline at end of file diff --git a/src/ServerStorage/Proxy/LevelProxy.luau b/src/ServerStorage/Proxy/LevelProxy.luau index 5f14643..0064e33 100644 --- a/src/ServerStorage/Proxy/LevelProxy.luau +++ b/src/ServerStorage/Proxy/LevelProxy.luau @@ -73,7 +73,6 @@ function LevelProxy:InitPlayer(Player: Player) -- 关卡目录下生成玩家目录 local spawnFloder = Utils:CreateFolder(Player.UserId, game.Workspace:FindFirstChild(STORE_NAME)) - print("spawnFloder: ", spawnFloder.Name) -- 新玩家数据初始化 if not ArchiveProxy.pData[Player.UserId][STORE_NAME] then diff --git a/src/ServerStorage/Proxy/MobAIProxy.luau b/src/ServerStorage/Proxy/MobAIProxy.luau deleted file mode 100644 index 282ec5e..0000000 --- a/src/ServerStorage/Proxy/MobAIProxy.luau +++ /dev/null @@ -1,4 +0,0 @@ --- 怪物AI代理 -local MobAIProxy = {} - -return MobAIProxy \ No newline at end of file diff --git a/src/ServerStorage/Proxy/MobsProxy/AI.luau b/src/ServerStorage/Proxy/MobsProxy/AI.luau new file mode 100644 index 0000000..7ccab98 --- /dev/null +++ b/src/ServerStorage/Proxy/MobsProxy/AI.luau @@ -0,0 +1,112 @@ +--[[ + 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 = {} + +-------------------------------------------------------------------------------- + +-- 获取两个单位之间的距离 +function AI:GetModelDistance(Unit1: Model, Unit2: Model): number + return (Unit1:GetPivot().Position - Unit2:GetPivot().Position).Magnitude +end + +function AI:GetClosestPlayer(Mob: any): (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.Humanoid.WalkToPart then + local Player = Players:GetPlayerFromCharacter(Mob.Humanoid.WalkToPart.Parent) + if Player then + ActivePlayer = Player + end + end + + -- 临时的 + for _, Player in Players:GetPlayers() do + if Mob.TargetPlayerUserID == Player.UserId then + 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 + 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 + +-------------------------------------------------------------------------------- + +-- Code to continue tracking 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.Stats.Died then + local Player, distance = AI:GetClosestPlayer(Mob) + local Enemy = Mob.Humanoid + + -- 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 + if distance > 5 then + Enemy:MoveTo(Player.Character:GetPivot().Position, Player.Character.PrimaryPart) + else + -- 停止移动 + Enemy:MoveTo(MobInstance:GetPivot().Position) + -- 触发攻击逻辑 + task.wait(Mob.Config.attackSpeed) + if Mob.Stats.Died then return end + if not Player then return end + if AI:GetModelDistance(MobInstance, Player.Character) <= 5 then + -- 调用伤害模块 + + end + end + end + else + -- ActiveMobs[MobInstance] = nil + end + end) + end + end +end) + +return AI \ No newline at end of file diff --git a/src/ServerStorage/Proxy/MobsProxy/init.luau b/src/ServerStorage/Proxy/MobsProxy/init.luau new file mode 100644 index 0000000..0814387 --- /dev/null +++ b/src/ServerStorage/Proxy/MobsProxy/init.luau @@ -0,0 +1,219 @@ +-- 怪物AI代理 +local MobsProxy = {} + +--> Services +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local ServerStorage = game:GetService("ServerStorage") +local Players = game:GetService("Players") + +--> Variables +local Utils = require(ReplicatedStorage.Tools.Utils) +local PlayerInfoProxy = require(ServerStorage.Proxy.PlayerInfoProxy) +local AI = require(script.AI) + +--> Json +local JsonMob = require(ReplicatedStorage.Json.Mob) + +--> Constants + +-------------------------------------------------------------------------------- + +-- 玩家怪物信息存储表 +MobsProxy.pData = {} +-- 初始化生成怪物目录 +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 +end + +local function FindMobPrefab(MobModelName: string) + return ReplicatedStorage.Mobs:FindFirstChild(MobModelName) +end + +-------------------------------------------------------------------------------- + +local Mob = {} +Mob.__index = Mob + +local LIMIT_ATTRIBUTE = { + "health" +} + +function Mob.new(Player: Player, MobId: number) + -- 怪物实例目录判断 + local playerMobsFolder = GetPlayerMobsFolder(Player) + if not playerMobsFolder then return end + -- 怪物数据获取 + local MobData = Utils:GetJsonData(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 newMobModel = MobInstance:Clone() + local HumanoidRootPart = newMobModel:FindFirstChild("HumanoidRootPart") :: BasePart + local mobHumanoid = newMobModel:FindFirstChild("Humanoid") :: Humanoid + + -- 生成表格数据 + local newMob = setmetatable({}, Mob) + newMob.Instance = newMobModel + newMob.Config = MobData + newMob.Root = HumanoidRootPart + newMob.Humanoid = mobHumanoid + newMob.Origin = HumanoidRootPart:GetPivot() + newMob.TargetPlayerUserID = Player.UserId + newMob.Connections = {} + newMob.Stats = {} + + -- 生成实例身上的配置数据 + local Attributes = Instance.new("Configuration") + Attributes.Name = "Attributes" + Attributes.Parent = newMob + for attributeKey, attributeValue in newMob.Config do + Attributes:SetAttribute(attributeKey, attributeValue) + -- 设置限制值 + if table.find(LIMIT_ATTRIBUTE, attributeKey) then + newMob.Config["max" .. attributeKey] = attributeValue + Attributes:SetAttribute("max" .. attributeKey, attributeValue) + end + + local conAttribute = newMob.AttributeChanged:Connect(function(attributeKey: string, attributeValue: number) + newMob:ChangeAttribute(attributeKey, attributeValue) + end) + table.insert(newMob.Connections, conAttribute) + end + + -- 配置角色状态数据 + local statsData = { + Died = false + } + local Stats = Instance.new("Configuration") + Stats.Name = "Stats" + Stats.Parent = newMob + for statKey, statValue in statsData do + newMob.Stats[statKey] = statValue + Stats:SetAttribute(statKey, statValue) + end + + -- 初始化原有功能数值 + mobHumanoid.WalkSpeed = MobData.walkSpeed + -- 放入关卡中 + newMobModel.Parent = playerMobsFolder + -- 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] + mobHumanoid:SetStateEnabled(HumanoidStateType, false) + if mobHumanoid:GetState() == HumanoidStateType then + mobHumanoid:ChangeState(Enum.HumanoidStateType.Running) + end + end + + -- 接入统一AI + -- Following has finished. Anchor assembly to optimize. + mobHumanoid.MoveToFinished:Connect(function() + if not AI:GetClosestPlayer(Mob) and not Mob.isDead then + HumanoidRootPart.Anchored = true + else + AI:StartTracking(Mob) + end + end) + return newMob +end + +function Mob:GetAttribute(attributeKey: string) + return self.Config[attributeKey], self.Instance.Attributes:GetAttribute(attributeKey) +end + +function Mob:ChangeAttribute(attributeKey: string, value: any) + local newValue = value + -- 限制最大值 + if table.find(LIMIT_ATTRIBUTE, attributeKey) then + if newValue > self.Config["max" .. attributeKey] then + newValue = self.Config["max" .. attributeKey] + end + end + -- 改变值 + self.Config[attributeKey] = newValue + self.Instance.Attributes:SetAttribute(attributeKey, newValue) + + -- 死亡判断 + if attributeKey == "health" and self.Stats.Died == false then + if self.Config[attributeKey] <= 0 then + self:Died() + end + end +end + +function Mob:GetState(state: string) + return self.Stats[state], self.Instance.Stats:GetAttribute(state) +end + +function Mob:ChangeState(state: string, value: any) + self.Stats[state] = value + self.Instance.Stats:SetAttribute(state, value) +end + +function Mob:Died() + self:ChangeState("Died", true) + MobsProxy:RemoveMob(self.Player, self.Instance) + for _, connection in self.Connections do + connection:Disconnect() + end + self.Connections = nil + self.Instance:Destroy() + self = nil +end + +-------------------------------------------------------------------------------- + +-- 给玩家创建怪物 +function MobsProxy:CreateMob(Player: Player, MobId: number) + local Mob = Mob.new(Player, MobId) + MobsProxy.pData[Player.UserId][Mob.Instance] = Mob + return Mob +end + +-- 清除玩家单个怪物 +function MobsProxy:RemoveMob(Player: Player, MobInstance: Instance) + local playerMobsFolder = GetPlayerMobsFolder(Player) + if not playerMobsFolder then return end + if not MobsProxy.pData[Player.UserId][MobInstance] then warn("Mob not found", MobInstance) return end + + MobsProxy.pData[Player.UserId][MobInstance] = nil +end + +-- 清除玩家所有的怪物 +function MobsProxy:CleanPlayerMobs(Player: Player) + if not Player then return end + for key, mobInstance in MobsProxy.pData[Player.UserId] do + mobInstance:Clean() + MobsProxy.pData[Player.UserId][key] = nil + end +end + +function MobsProxy:InitPlayer(Player: Player) + local pData = Utils:GetPlayerDataFolder(Player) + if not pData then return end + Utils:CreateFolder(Player.UserId, MobsFolder) + + MobsProxy.pData[Player.UserId] = {} +end + +function MobsProxy:OnPlayerRemoving(Player: Player) + local pData = Utils:GetPlayerDataFolder(Player) + if not pData then return end + local MobsFolder = pData:FindFirstChild("Mobs"):FindFirstChild(Player.UserId) + if MobsFolder then MobsFolder:Destroy() end +end + +Players.PlayerRemoving:Connect(function(Player: Player) + MobsProxy:OnPlayerRemoving(Player) +end) + +return MobsProxy \ No newline at end of file