266 lines
8.6 KiB
Plaintext
266 lines
8.6 KiB
Plaintext
-- 刷怪脚本:带 AI 与等级设置
|
||
|
||
local commands = {}
|
||
-- 全局设置:是否把坐标保留 2 位小数
|
||
local stripCoordinates = true -- 设为 false 显示完整坐标
|
||
-- 全局设置:是否在游戏内聊天输出
|
||
local showInChat = false -- 设为 false 仅控制台输出
|
||
-- 全局设置:网格间距(按网格铺开生成时的距离)
|
||
local gridSpacing = 3 -- 默认 3 米
|
||
-- 全局设置:是否全部生成在同一坐标
|
||
local spawnInSameLocation = false -- 设为 true 则同点生成
|
||
-- 回调序列号(用于生成唯一的回调名称)
|
||
local callbackSeq = 0
|
||
|
||
|
||
-- 工具函数
|
||
local function round2(n)
|
||
return math.floor(n * 100 + 0.5) / 100
|
||
end
|
||
|
||
local function countTable(t)
|
||
local n = 0
|
||
for _ in pairs(t) do n = n + 1 end
|
||
return n
|
||
end
|
||
|
||
-- 加载外部敌人名称映射表(使用 loadfile)
|
||
local function loadNameOverrides()
|
||
local paths = {
|
||
"OpenWF/Scripts/EnemyNameOverrides.pluto",
|
||
"Scripts/EnemyNameOverrides.pluto",
|
||
}
|
||
|
||
for _, path in ipairs(paths) do
|
||
local chunk, err = loadfile(path)
|
||
if chunk then
|
||
local success, data = pcall(chunk)
|
||
if success and type(data) == "table" then
|
||
print("✓ 已加载 " .. countTable(data) .. " 个敌人名称映射(" .. path .. ")")
|
||
return data
|
||
end
|
||
end
|
||
end
|
||
|
||
print("⚠ 警告:无法加载 EnemyNameOverrides.pluto,所有敌人需要使用完整路径")
|
||
return {}
|
||
end
|
||
|
||
local nameOverrides = loadNameOverrides()
|
||
|
||
|
||
-- 基于覆盖表构建 名称<->路径 的双向索引(兼容旧格式:路径->名称,或新格式:名称->路径)
|
||
local nameToPath = {}
|
||
local pathToName = {}
|
||
local function normalizeName(s)
|
||
if not s then return s end
|
||
-- 去掉所有空白字符,避免“苦 难”“苦难 ”等变体匹配失败
|
||
return (s:gsub("%s+", ""))
|
||
end
|
||
local function addIndex(name, path)
|
||
if not name or name == "" or not path or path == "" then return end
|
||
if not nameToPath[name] then nameToPath[name] = path end
|
||
local n2 = normalizeName(name)
|
||
if n2 ~= name and not nameToPath[n2] then nameToPath[n2] = path end
|
||
if not pathToName[path] then pathToName[path] = name end
|
||
end
|
||
local function rebuildIndexes()
|
||
nameToPath = {}
|
||
pathToName = {}
|
||
for k, v in pairs(nameOverrides) do
|
||
if type(k) == "string" and type(v) == "string" and k ~= "" and v ~= "" then
|
||
if k:sub(1,1) == "/" then
|
||
addIndex(v, k)
|
||
else
|
||
addIndex(k, v)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
rebuildIndexes()
|
||
|
||
local function logSpawn(enemyName, pos, level, note)
|
||
local x, y, z = pos.x, pos.y, pos.z
|
||
if stripCoordinates then
|
||
x = round2(x)
|
||
y = round2(y)
|
||
z = round2(z)
|
||
end
|
||
local tag = note and (" " .. note) or ""
|
||
local msg = string.format("已生成 '%s'%s 坐标: %.2f, %.2f, %.2f 等级: %d", enemyName, tag, x, y, z, level)
|
||
print(msg)
|
||
if showInChat then
|
||
chat_system_reply(msg)
|
||
end
|
||
end
|
||
|
||
local function resolveDisplayName(path, agent, fallback)
|
||
return pathToName[path] or fallback
|
||
end
|
||
|
||
local function computeSpawnPosition(basePosition, index, count)
|
||
if spawnInSameLocation then
|
||
return basePosition
|
||
end
|
||
local gridSize = math.ceil(math.sqrt(count))
|
||
local i0 = index - 1 -- zero-based for grid math
|
||
local col = i0 % gridSize
|
||
local row = math.floor(i0 / gridSize)
|
||
return basePosition + Vector(col * gridSpacing, 0, row * gridSpacing)
|
||
end
|
||
|
||
-- 实际生成敌人的函数(提取重复代码)
|
||
local function spawnEnemies(enemyType, path, count, level, note)
|
||
local basePosition = gRegion:GetLocalPlayerAvatar():GetPosition()
|
||
local npcMgr = gRegion:GetNpcMgr()
|
||
local agentType = Type(path)
|
||
|
||
for i = 1, count do
|
||
local spawnPosition = computeSpawnPosition(basePosition, i, count)
|
||
local agent = npcMgr:CreateAgentAtPosition(agentType, spawnPosition, ZERO_ROTATION, EMPTY_SYMBOL, level)
|
||
local displayName = resolveDisplayName(path, agent, enemyType)
|
||
logSpawn(displayName, spawnPosition, level, note)
|
||
end
|
||
end
|
||
|
||
-- 请求资源并在回调后生成
|
||
local function requestAndSpawn(enemyType, path, count, level, callbackName)
|
||
print("请求加载资源: " .. path)
|
||
gGameRules:RequestResource(path, callbackName)
|
||
|
||
local callbackReceived = false
|
||
local fallbackTriggered = false
|
||
local fallbackTimeout = 180 -- 超时 180 帧(约 3 秒 @ 60fps)
|
||
|
||
repeat
|
||
while evt := owf_next_event() do
|
||
if evt.type == OWF_EVT_CALLBACK and evt.name == callbackName then
|
||
callbackReceived = true
|
||
print("已接收回调,资源加载完成: " .. path)
|
||
spawnEnemies(enemyType, path, count, level, nil)
|
||
return
|
||
end
|
||
end
|
||
|
||
fallbackTimeout = fallbackTimeout - 1
|
||
if fallbackTimeout <= 0 and not callbackReceived then
|
||
fallbackTriggered = true
|
||
print("未在超时时间内收到回调,进入直接生成回退路径。")
|
||
break
|
||
end
|
||
yield()
|
||
until callbackReceived
|
||
|
||
if fallbackTriggered then
|
||
spawnEnemies(enemyType, path, count, level, "(回退)")
|
||
elseif not callbackReceived then
|
||
print("错误:资源加载失败或不可用: " .. path)
|
||
end
|
||
end
|
||
|
||
|
||
|
||
commands["生成"] = function(text)
|
||
local parts = text:split(" ")
|
||
local enemyOrPath = parts[2]
|
||
|
||
-- 参数验证
|
||
if not enemyOrPath or enemyOrPath == "" then
|
||
local usage = "用法: /生成 <名称或路径> [数量] [等级]\n示例: /生成 苦难 5 100"
|
||
print(usage)
|
||
if showInChat then chat_system_reply(usage) end
|
||
return
|
||
end
|
||
|
||
local count = tonumber(parts[3]) or 1
|
||
local level = tonumber(parts[4]) or 10
|
||
|
||
local path = enemyOrPath
|
||
-- 若输入不是路径,尝试按"名称->路径"映射解析
|
||
if path and path:sub(1, 1) ~= "/" then
|
||
local mapped = nameToPath[path] or nameToPath[normalizeName(path)]
|
||
if mapped then
|
||
path = mapped
|
||
else
|
||
print("警告:未找到名称 '" .. enemyOrPath .. "' 的映射,将尝试作为路径使用")
|
||
end
|
||
end
|
||
|
||
print(string.format("开始生成 | 路径: %s | 数量: %d | 等级: %d", path, count, level))
|
||
|
||
callbackSeq = callbackSeq + 1
|
||
local callbackName = "streamed_enemy_" .. tostring(callbackSeq)
|
||
requestAndSpawn(enemyOrPath, path, count, level, callbackName)
|
||
end
|
||
|
||
|
||
|
||
-- 调试:查询名称映射情况
|
||
commands["查名"] = function(text)
|
||
local parts = text:split(" ")
|
||
local key = parts[2]
|
||
if not key or key == "" then
|
||
local usage = "用法: /查名 <名称>"
|
||
print(usage)
|
||
if showInChat then chat_system_reply(usage) end
|
||
return
|
||
end
|
||
local norm = normalizeName(key)
|
||
local direct = nameOverrides[key]
|
||
local directNorm = nameOverrides[norm]
|
||
local mapped = nameToPath[key] or nameToPath[norm]
|
||
print(string.format("查名 | key='%s' norm='%s' | 覆盖表[key]=%s 覆盖表[norm]=%s | 映射=%s",
|
||
key, norm, tostring(direct), tostring(directNorm), tostring(mapped)))
|
||
end
|
||
|
||
-- 重载命令(重新从外部文件加载)
|
||
commands["重载名表"] = function(text)
|
||
local newData = loadNameOverrides()
|
||
if newData and countTable(newData) > 0 then
|
||
nameOverrides = newData
|
||
rebuildIndexes()
|
||
local msg = "✓ 名称映射已重载,条目: " .. tostring(countTable(nameOverrides))
|
||
print(msg)
|
||
if showInChat then chat_system_reply(msg) end
|
||
else
|
||
local msg = "✗ 重载失败:无法加载 EnemyNameOverrides.pluto"
|
||
print(msg)
|
||
if showInChat then chat_system_reply(msg) end
|
||
end
|
||
end
|
||
|
||
-- 帮助命令
|
||
commands["帮助"] = function(text)
|
||
local help = [[
|
||
可用命令:
|
||
/生成 <名称或路径> [数量] [等级] - 生成敌人
|
||
/查名 <名称> - 查询敌人名称映射
|
||
/重载名表 - 重新加载外部名称映射文件
|
||
/帮助 - 显示此帮助信息
|
||
|
||
示例:
|
||
/生成 苦难 5 100
|
||
/生成 /Lotus/Types/Enemies/Acolytes/AreaCasterAcolyteAgent 1 150
|
||
/查名 苦难
|
||
]]
|
||
print(help)
|
||
if showInChat then chat_system_reply(help) end
|
||
end
|
||
|
||
-- 订阅聊天命令并处理事件
|
||
for prefix in commands do
|
||
chat_subscribe_prefix("/" .. prefix, true)
|
||
end
|
||
|
||
repeat
|
||
while evt := owf_next_event() do
|
||
if evt.type == OWF_EVT_SUBMIT_CHAT_MESSAGE and evt.text and evt.text:sub(1,1) == "/" then
|
||
local cmd = evt.text:match("^/([^%s]+)")
|
||
local f = cmd and commands[cmd]
|
||
if f then f(evt.text) end
|
||
end
|
||
end
|
||
until yield()
|
||
|
||
|
||
-- 示例:/生成 苦难 5 100 |