diff --git a/excel/level.xlsx b/excel/level.xlsx index da1b8b4..2186fd1 100644 Binary files a/excel/level.xlsx and b/excel/level.xlsx differ diff --git a/export.py b/export.py index f3edd4f..0c7d731 100644 --- a/export.py +++ b/export.py @@ -11,16 +11,13 @@ if not os.path.exists(output_dir): def parse_array_field(value, elem_type): """ - 解析数组类型字段,支持[1,2,3]或1,2,3写法,自动去除空格和空字符串。 - elem_type: 'int', 'float', 'string'等 + 解析一维数组类型字段,支持[1,2,3]或1,2,3写法。 """ if value is None: return [] s = str(value).strip() - # 支持带中括号写法 if s.startswith("[") and s.endswith("]"): s = s[1:-1] - # 支持英文逗号和中文逗号 items = re.split(r'[,,]', s) result = [] for v in items: @@ -41,6 +38,32 @@ def parse_array_field(value, elem_type): result.append(v) return result +def parse_2d_array_field(value, elem_type): + """ + 解析二维数组类型字段,支持[1,2],[3,4]或[[1,2],[3,4]]等写法。 + 保证导出始终为双数组结构。 + """ + if value is None or str(value).strip() == "": + return [] + s = str(value).strip() + # 去除最外层中括号 + if s.startswith("[[") and s.endswith("]]"): + s = s[1:-1] + # 按 '],[' 拆分 + parts = re.split(r'\]\s*,\s*\[', s) + result = [] + for part in parts: + part = part.strip("[] ") + arr = parse_array_field(part, elem_type) + result.append(arr) + # 如果内容其实是一维数组(如单元格内容为1,2,3),也包一层 + if len(result) == 1 and not isinstance(result[0], list): + result = [parse_array_field(s, elem_type)] + # 如果内容为空但原始字符串非空,也包一层 + if not result and s: + result = [parse_array_field(s, elem_type)] + return result + for filename in os.listdir(excel_dir): if filename.endswith('.xlsx'): filepath = os.path.join(excel_dir, filename) @@ -65,7 +88,10 @@ for filename in os.listdir(excel_dir): filtered_row = [row[i] if i < len(row) else None for i in valid_indices] row_dict = {} for h, t, v in zip(valid_headers, valid_types, filtered_row): - if t.endswith("[]"): + if t.endswith("[][]"): + elem_type = t[:-4] + row_dict[h] = parse_2d_array_field(v, elem_type) + elif t.endswith("[]"): elem_type = t[:-2] row_dict[h] = parse_array_field(v, elem_type) else: diff --git a/src/ReplicatedStorage/Json/Level.json b/src/ReplicatedStorage/Json/Level.json index c4dca4d..162b75c 100644 --- a/src/ReplicatedStorage/Json/Level.json +++ b/src/ReplicatedStorage/Json/Level.json @@ -1,22 +1,22 @@ [ -{"id":1,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[1,1,10,2,1,1,10,2]}, -{"id":2,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[1,1,10,2,1,1,10,2,1,1,10,2,1,1,10,2]}, -{"id":3,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[1,1,10,2,1,1,10,2]}, -{"id":4,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[1,1,10,2,1,1,10,2,1,1,10,2,1,1,10,2]}, -{"id":5,"type":2,"timeLimit":60,"atkBonus":1000,"hpBonus":1000,"wave":[1000]}, -{"id":6,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[1,1,10,2,1,1,10,2]}, -{"id":7,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[1,1,10,2,1,1,10,2,1,1,10,2,1,1,10,2]}, -{"id":8,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[1,1,10,2,1,1,10,2]}, -{"id":9,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[1,1,10,2,1,1,10,2,1,1,10,2,1,1,10,2]}, -{"id":10,"type":2,"timeLimit":60,"atkBonus":1000,"hpBonus":1000,"wave":[1000]}, -{"id":11,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[1,1,10,2,1,1,10,2]}, -{"id":12,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[1,1,10,2,1,1,10,2,1,1,10,2,1,1,10,2]}, -{"id":13,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[1,1,10,2,1,1,10,2]}, -{"id":14,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[1,1,10,2,1,1,10,2,1,1,10,2,1,1,10,2]}, -{"id":15,"type":2,"timeLimit":60,"atkBonus":1000,"hpBonus":1000,"wave":[1000]}, -{"id":16,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[1,1,10,2,1,1,10,2]}, -{"id":17,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[1,1,10,2,1,1,10,2,1,1,10,2,1,1,10,2]}, -{"id":18,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[1,1,10,2,1,1,10,2]}, -{"id":19,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[1,1,10,2,1,1,10,2,1,1,10,2,1,1,10,2]}, -{"id":20,"type":2,"timeLimit":60,"atkBonus":1000,"hpBonus":1000,"wave":[1000]} +{"id":1,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[[10,1,1,10,2,1],[10,1,1,10,2,1]]}, +{"id":2,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[[10,1,1,10,2,1],[10,1,1,10,2,1],[10,1,1,10,2,1],[10,1,1,10,2,1]]}, +{"id":3,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[[10,1,1,10,2,1],[10,1,1,10,2,1]]}, +{"id":4,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[[10,1,1,10,2,1],[10,1,1,10,2,1],[10,1,1,10,2,1],[10,1,1,10,2,1]]}, +{"id":5,"type":2,"timeLimit":60,"atkBonus":1000,"hpBonus":1000,"wave":[[10,1000,1]]}, +{"id":6,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[[10,1,1,10,2,1],[10,1,1,10,2,1]]}, +{"id":7,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[[10,1,1,10,2,1],[10,1,1,10,2,1],[10,1,1,10,2,1],[10,1,1,10,2,1]]}, +{"id":8,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[[10,1,1,10,2,1],[10,1,1,10,2,1]]}, +{"id":9,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[[10,1,1,10,2,1],[10,1,1,10,2,1],[10,1,1,10,2,1],[10,1,1,10,2,1]]}, +{"id":10,"type":2,"timeLimit":60,"atkBonus":1000,"hpBonus":1000,"wave":[[10,1000,1]]}, +{"id":11,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[[10,1,1,10,2,1],[10,1,1,10,2,1]]}, +{"id":12,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[[10,1,1,10,2,1],[10,1,1,10,2,1],[10,1,1,10,2,1],[10,1,1,10,2,1]]}, +{"id":13,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[[10,1,1,10,2,1],[10,1,1,10,2,1]]}, +{"id":14,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[[10,1,1,10,2,1],[10,1,1,10,2,1],[10,1,1,10,2,1],[10,1,1,10,2,1]]}, +{"id":15,"type":2,"timeLimit":60,"atkBonus":1000,"hpBonus":1000,"wave":[[10,1000,1]]}, +{"id":16,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[[10,1,1,10,2,1],[10,1,1,10,2,1]]}, +{"id":17,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[[10,1,1,10,2,1],[10,1,1,10,2,1],[10,1,1,10,2,1],[10,1,1,10,2,1]]}, +{"id":18,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[[10,1,1,10,2,1],[10,1,1,10,2,1]]}, +{"id":19,"type":1,"timeLimit":null,"atkBonus":1000,"hpBonus":1000,"wave":[[10,1,1,10,2,1],[10,1,1,10,2,1],[10,1,1,10,2,1],[10,1,1,10,2,1]]}, +{"id":20,"type":2,"timeLimit":60,"atkBonus":1000,"hpBonus":1000,"wave":[[10,1000,1]]} ] \ No newline at end of file diff --git a/src/ServerStorage/Base/Character.luau b/src/ServerStorage/Base/Character.luau index 27ed32c..4a9297d 100644 --- a/src/ServerStorage/Base/Character.luau +++ b/src/ServerStorage/Base/Character.luau @@ -110,3 +110,5 @@ function Character:Died() self.Instance:Destroy() self = nil end + +return Character \ No newline at end of file diff --git a/src/ServerStorage/Modules/MobLib/init.luau b/src/ServerStorage/Modules/MobLib/init.luau index f0e9093..3521715 100644 --- a/src/ServerStorage/Modules/MobLib/init.luau +++ b/src/ServerStorage/Modules/MobLib/init.luau @@ -41,9 +41,10 @@ end function MobLib.new(MobInstance: Model): Mobs.Mob local HumanoidRootPart = MobInstance:FindFirstChild("HumanoidRootPart") :: BasePart - local Enemy = MobInstance:FindFirstChild("Enemy") :: Humanoid + 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 diff --git a/src/ServerStorage/Proxy/LevelProxy.luau b/src/ServerStorage/Proxy/LevelProxy.luau index 0064e33..27f55a4 100644 --- a/src/ServerStorage/Proxy/LevelProxy.luau +++ b/src/ServerStorage/Proxy/LevelProxy.luau @@ -9,6 +9,8 @@ local Players = game:GetService("Players") --> Variables local Utils = require(ReplicatedStorage.Tools.Utils) local ArchiveProxy = require(ServerStorage.Proxy.ArchiveProxy) +local MobsProxy = require(ServerStorage.Proxy.MobsProxy) +local TypeList = require(ServerStorage.Base.TypeList) --> Json local JsonLevel = require(ReplicatedStorage.Json.Level) @@ -60,6 +62,48 @@ local function ExtraAddPlayerLevel(Player: Player, LevelData: table) end end +local EXCEPT_KEY = { "Task", "Mobs"} +local function ChangeValue(Player: Player, Folder: Instance, LevelKey: string, LevelValue: any) + if not Player or not Folder or not LevelKey or not LevelValue then return end + local ValueInstance = Folder:FindFirstChild(LevelKey) + if not ValueInstance then return end + + local storeTable + if Folder.Name == "Challenge" then + storeTable = LevelProxy.pData[Player.UserId] + else + storeTable = ArchiveProxy.pData[Player.UserId][STORE_NAME].Progress + end + + storeTable[LevelKey] = LevelValue + if not table.find(EXCEPT_KEY, LevelKey) then ValueInstance.Value = LevelValue end +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) + + -- 怪物清除判断 + local LevelData = Utils:GetJsonData(JsonLevel, LevelProxy.pData[Player.UserId].LevelId) + if LevelProxy.pData[Player.UserId].SpawnWaveFinish and #LevelProxy.pData[Player.UserId].Mobs == 0 then + if LevelProxy.pData[Player.UserId].NowWave < #LevelData["wave"] then + -- 波数增长 + LevelProxy.pData[Player.UserId].NowWave = LevelProxy.pData[Player.UserId].NowWave + 1 + -- 新波次重置怪物生成状态标记 + local ChallengeFolder = LevelFolder:FindFirstChild("Challenge") + ChangeValue(Player, ChallengeFolder, "SpawnWaveFinish", false) + elseif LevelProxy.pData[Player.UserId].NowWave >= #LevelData["wave"] then + -- 结束判断 + LevelProxy:ChallengeEnd(Player, true) + end + end + break + end +end + -------------------------------------------------------------------------------- function LevelProxy:InitPlayer(Player: Player) @@ -68,6 +112,7 @@ function LevelProxy:InitPlayer(Player: Player) local LevelFolder = Utils:CreateFolder(STORE_NAME, pData) local ProgressFolder = Utils:CreateFolder("Progress", LevelFolder) local DungeonFolder = Utils:CreateFolder("Dungeon", LevelFolder) + local ChallengeFolder = Utils:CreateFolder("Challenge", LevelFolder) -- 当前关卡状态 Utils:CreateFolder("Stats", LevelFolder) @@ -88,29 +133,108 @@ function LevelProxy:InitPlayer(Player: Player) for LevelKey, LevelValue in ArchiveProxy.pData[Player.UserId][STORE_NAME].Progress do CreateLevelInstance(Player, ProgressFolder, LevelKey, LevelValue) end + + -- 本地内容初始化(关卡挑战信息,不存储) + if not LevelProxy.pData then LevelProxy.pData = {} end + if not LevelProxy.pData[Player.UserId] then LevelProxy.pData[Player.UserId] = {} end + LevelProxy.pData[Player.UserId].Task = nil + LevelProxy.pData[Player.UserId].Time = 0 + LevelProxy.pData[Player.UserId].LevelId = 0 + LevelProxy.pData[Player.UserId].MaxTime = 0 + LevelProxy.pData[Player.UserId].IsBoss = false + LevelProxy.pData[Player.UserId].NowWave = 0 + LevelProxy.pData[Player.UserId].ShouldWave = 0 + LevelProxy.pData[Player.UserId].SpawnWaveFinish = false + LevelProxy.pData[Player.UserId].Mobs = {} + + -- 关卡挑战信息前端 + for key, value in LevelProxy.pData[Player.UserId] do + if key == "Task" or key == "Mobs" then continue end + CreateLevelInstance(Player, ChallengeFolder, key, value) + end end -- 挑战关卡(挑战副本用另一个函数) function LevelProxy:ChallengeLevel(Player: Player, LevelId: number) + local LevelData = Utils:GetJsonData(JsonLevel, LevelId) + if not LevelData then warn("Level Data not found", LevelId) return end -- 给前端传数据,做表现 -- 场景后端生成 -- 后端生成当前关卡状态数据 + local LevelFolder = GetPlayerLevelWorkspaceFolder(Player.UserId) + local ChallengeFolder = LevelFolder:FindFirstChild("Challenge") + if not ChallengeFolder then return end + local levelTask = task.defer(function() + ChangeValue(Player, ChallengeFolder, "IsBoss", LevelData.type == 1 and true or false) + ChangeValue(Player, ChallengeFolder, "LevelId", LevelId) + ChangeValue(Player, ChallengeFolder, "Time", 0) + ChangeValue(Player, ChallengeFolder, "NowWave", 0) + ChangeValue(Player, ChallengeFolder, "ShouldWave", 1) + ChangeValue(Player, ChallengeFolder, "MaxTime", LevelData.timeLimit or 0) + ChangeValue(Player, ChallengeFolder, "Mobs", {}) + + if LevelData.timeLimit then + while task.wait(1) do + ChangeValue(Player, ChallengeFolder, "Time", LevelProxy.pData[Player.UserId].Time + 1) + + -- 关卡生成 + if LevelProxy.pData[Player.UserId].Wave < #LevelData.wave then + ChangeValue(Player, ChallengeFolder, "Wave", LevelProxy.pData[Player.UserId].Wave + 1) + end + if LevelProxy.pData[Player.UserId].NowWave < LevelProxy.pData[Player.UserId].ShouldWave then + 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] + local mobCount = waveData[i + 2] + for _ = 1, mobCount do + local mob = MobsProxy:CreateMob(Player, mobId, LevelData.atkBonus, LevelData.hpBonus, OnMobDied) + table.insert(LevelProxy.pData[Player.UserId].Mobs, mob) + end + end + ChangeValue(Player, ChallengeFolder, "SpawnWaveFinish", true) + end + -- 时间结束 + if LevelProxy.pData[Player.UserId].Time >= LevelData.timeLimit then + self:ChallengeEnd(Player, false) + break + end + end + end + end) + ChangeValue(Player, ChallengeFolder, "Task", levelTask) end -- 挑战结束 -function LevelProxy:ChallengeEnd(Player: Player) +function LevelProxy:ChallengeEnd(Player: Player, result: boolean) + local pData = Utils:GetPlayerDataFolder(Player) + 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 + LevelProxy.pData[Player.UserId].Mobs = {} + -- 判断玩家是否通关 - -- 通关后,没到最大关卡,关卡进度+1 - -- 到达最大关卡不做处理 + if result then + ChangeValue(Player, ProgressFolder, "LevelId", LevelProxy.pData[Player.UserId].LevelId + 1) + end end function LevelProxy:OnPlayerRemoving(Player: Player) + -- 关卡文件夹清除 local PlayerLevelFolder = GetPlayerLevelWorkspaceFolder(Player.UserId) if PlayerLevelFolder then PlayerLevelFolder:Destroy() end + -- 关卡存储数据清除 + if LevelProxy.pData[Player.UserId].Task then + LevelProxy.pData[Player.UserId].Task:Cancel() + end + LevelProxy.pData[Player.UserId] = nil end -- ReplicatedStorage.Remotes.PlayerRemoving.Event:Connect(function(PlayerUserId: string) diff --git a/src/ServerStorage/Proxy/MobsProxy/AI.luau b/src/ServerStorage/Proxy/MobsProxy/AI.luau index 7ccab98..bdbc1ba 100644 --- a/src/ServerStorage/Proxy/MobsProxy/AI.luau +++ b/src/ServerStorage/Proxy/MobsProxy/AI.luau @@ -10,22 +10,24 @@ --> Services local Players = game:GetService("Players") +local ServerStorage = game:GetService("ServerStorage") --> Dependencies -local MobList = require(script.Parent.MobList) +local TypeList = require(ServerStorage.Base.TypeList) --> Variables +local DamageProxy = require(ServerStorage.Proxy.DamageProxy) local ActiveMobs = {} local AI = {} -------------------------------------------------------------------------------- -- 获取两个单位之间的距离 -function AI:GetModelDistance(Unit1: Model, Unit2: Model): number +function AI:GetModelDistance(Unit1: TypeList.Character, Unit2: TypeList.Character): number return (Unit1:GetPivot().Position - Unit2:GetPivot().Position).Magnitude end -function AI:GetClosestPlayer(Mob: any): (Player?, number?) +function AI:GetClosestPlayer(Mob: TypeList.Character): (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. @@ -97,7 +99,13 @@ task.defer(function() if not Player then return end if AI:GetModelDistance(MobInstance, Player.Character) <= 5 then -- 调用伤害模块 - + DamageProxy:TakeDamage(MobInstance, Player.Character, { + { + Damage = 10, + DamageType = DamageProxy.DamageType.PHYSICAL, + DamageTag = DamageProxy.DamageTag.NORMAL + } + }) end end end diff --git a/src/ServerStorage/Proxy/MobsProxy/init.luau b/src/ServerStorage/Proxy/MobsProxy/init.luau index e86ab1c..650f15a 100644 --- a/src/ServerStorage/Proxy/MobsProxy/init.luau +++ b/src/ServerStorage/Proxy/MobsProxy/init.luau @@ -11,9 +11,10 @@ local Utils = require(ReplicatedStorage.Tools.Utils) local PlayerInfoProxy = require(ServerStorage.Proxy.PlayerInfoProxy) local AI = require(script.AI) local Character = require(ServerStorage.Base.Character) +local TypeList = require(ServerStorage.Base.TypeList) --> Json -local JsonMob = require(ReplicatedStorage.Json.Mob) +local JsonMob = require(ReplicatedStorage.Json.Enemy) --> Constants @@ -42,7 +43,7 @@ end local Mob = {} Mob.__index = Mob -function Mob.new(Player: Player, MobId: number) +function Mob.new(Player: Player, MobId: number, OnMobDied: ((Player: Player, Mob: TypeList.Character) -> ())?) -- 获取玩家怪物目录 local playerMobsFolder = GetPlayerMobsFolder(Player) if not playerMobsFolder then return end @@ -64,6 +65,9 @@ function Mob.new(Player: Player, MobId: number) -- 放入关卡中 newMobModel.Parent = playerMobsFolder + + -- 死亡函数 + if OnMobDied then Mob.OnDied = OnMobDied end -- 接入统一AI self.Humanoid.MoveToFinished:Connect(function() @@ -78,14 +82,18 @@ end function Mob:Died() MobsProxy:RemoveMob(self.Player, self.Instance) + if self.OnDied then self.OnDied(self.Player, self) end Character.Died(self) end -------------------------------------------------------------------------------- -- 给玩家创建怪物 -function MobsProxy:CreateMob(Player: Player, MobId: number) - local Mob = Mob.new(Player, MobId) +function MobsProxy:CreateMob(Player: Player, MobId: number, AtkBonus: number?, HpBonus: number?, OnMobDied: ((Player: Player, Mob: TypeList.Character) -> ())?) + local Mob = Mob.new(Player, MobId, OnMobDied) + -- 关卡系数 + 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 MobsProxy.pData[Player.UserId][Mob.Instance] = Mob return Mob end