From fc39c8c3b3c12a2abfa441d8066c37b990d028d6 Mon Sep 17 00:00:00 2001 From: Ggafrik <906823881@qq.com> Date: Fri, 4 Jul 2025 01:08:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- excel/enemy.xlsx | Bin 8945 -> 9002 bytes src/ReplicatedStorage/Json/Enemy.json | 6 +- src/ServerStorage/Proxy/ArchiveProxy.luau | 2 - src/ServerStorage/Proxy/DamageProxy.luau | 36 ++++ src/ServerStorage/Proxy/LevelProxy.luau | 1 - src/ServerStorage/Proxy/MobAIProxy.luau | 4 - src/ServerStorage/Proxy/MobsProxy/AI.luau | 112 ++++++++++ src/ServerStorage/Proxy/MobsProxy/init.luau | 219 ++++++++++++++++++++ 8 files changed, 370 insertions(+), 10 deletions(-) create mode 100644 src/ServerStorage/Proxy/DamageProxy.luau delete mode 100644 src/ServerStorage/Proxy/MobAIProxy.luau create mode 100644 src/ServerStorage/Proxy/MobsProxy/AI.luau create mode 100644 src/ServerStorage/Proxy/MobsProxy/init.luau diff --git a/excel/enemy.xlsx b/excel/enemy.xlsx index 212ea3b043f4a5b59866bc23d213070971038d71..bc9b0adf11d644daef797fed5c72f449fd7239b1 100644 GIT binary patch delta 2398 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 z02BZK00000000000002elSd*a8;98TSVaK<09OJ401*HH00000000000000=lZhfe Q0mhTaA|nQrAOHXW0L&L!o&W#< delta 2374 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 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