This commit is contained in:
Ggafrik 2025-07-07 00:03:47 +08:00
parent 40e1eb3462
commit 42dbc86b3a
8 changed files with 206 additions and 37 deletions

Binary file not shown.

View File

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

View File

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

View File

@ -110,3 +110,5 @@ function Character:Died()
self.Instance:Destroy()
self = nil
end
return Character

View File

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

View File

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

View File

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

View File

@ -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
@ -65,6 +66,9 @@ function Mob.new(Player: Player, MobId: number)
-- 放入关卡中
newMobModel.Parent = playerMobsFolder
-- 死亡函数
if OnMobDied then Mob.OnDied = OnMobDied end
-- 接入统一AI
self.Humanoid.MoveToFinished:Connect(function()
if not AI:GetClosestPlayer(Mob) and not Mob.isDead then
@ -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