更新-玩家ai

需调试bug
This commit is contained in:
Ggafrik 2025-07-07 23:52:31 +08:00
parent d4435197ca
commit 962aedcb0d
26 changed files with 509 additions and 952 deletions

BIN
excel/cha.xlsx Normal file

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,3 @@
[
{"id":1,"name":1,"attack":10,"hp":100,"walkSpeed":10,"attackSpeed":2}
]

View File

@ -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"}
]

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 {}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,10 +0,0 @@
export type Mob = {
Instance: Model,
Config: any,
Root: BasePart,
Enemy: Humanoid,
Origin: CFrame,
Respawn: (Mob) -> ()
}
return {}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,4 +1,4 @@
-- 玩家基础信息代理
-- 玩家通用信息
local PlayerInfoProxy = {}
--> Services

View File

@ -1,6 +0,0 @@
-- 玩家循环代理
local PlayerLoopProxy = {}
return PlayerLoopProxy