EternalTD 关卡与地图
EternalTD 中的"关卡"是位于 plugins/EternalTD/levels/ 中的 YAML 配置,并与 plugins/EternalTD/worlds/ 中的模板世界文件夹配对。当玩家加入时,EternalTD 会将模板世界克隆到服务器的世界容器中,并在该克隆副本中运行会话。
关卡配置字段
| 字段 | 类型 | 默认值 | 备注 |
|---|---|---|---|
isEnabled | bool | true | 已禁用的关卡在加载时会被跳过 |
levelName | string | null | 在消息、计分板和 NPC 菜单中显示的名称 |
levelDescription | string list | [] | 在 NPC 菜单中显示的行。支持 $highscoreWave 和 $highscorePlayer 占位符 |
worldName | string | null | plugins/EternalTD/worlds/ 下的模板文件夹名称 |
startLocation | string list | null | 生物生成位置的序列化位置列表 |
endLocation | string list | null | 生物所走向的位置列表("红色"瓦片) |
levelLocations | string list | null | 关卡中每一个可行走的网格方格,注册地板选择时自动生成 |
wavesConfigFile | string | 必填 | 链接的波次配置的文件名(waves/<name>.yml) |
waveCount | int | -1 | 缓存的波次计数,当前仅供参考 |
highscoreWave | int | 0 | 该关卡达到的最佳波次 |
highscorePlayerName | string | "no one" | 创下分数的玩家显示名称 |
environment | enum | NORMAL | 加载克隆世界时使用的世界环境 |
整个系统使用的网格大小为每个逻辑方格 3 格(代码中的常量 GRID_SIZE)。
位置字符串格式
EternalTD 中的位置以下列形式的逗号分隔字符串进行序列化:
worldName,x,y,z,yaw,pitch
通常无需手动编写 —— /etd selectfloor 加注册命令会为你计算并保存它们。
世界生命周期
当玩家加入关卡时:
- EternalTD 检查该玩家是否已有一份正在复制中的副本。在第一份仍在复制时再次执行
/etd join会被拒绝,并提示 "Your level is already being prepared." - 通过
worldName在plugins/EternalTD/worlds/中查找模板文件夹;如果世界文件夹缺失、关联的波次文件加载失败,或者世界名不是安全的文件夹名,则拒绝加入。 - 预留下一个空闲的数字后缀(
<worldName>_0、<worldName>_1……)。预留过程是同步的,并会记住那些已被预留但尚未写入磁盘的名称,因此两名玩家同时加入不会撞上同一个实例名。 - 模板会在主线程之外被复制到服务器世界容器中,因此服务器不会因为地图过大而卡顿。
- 通过 MagmaCore 的
TemporaryWorldManager将克隆世界加载为临时空世界,从而隔离 Paper 26.1+ 迁移,并且缺失的区块会以虚空形式返回,而不是重新生成地形。 - 玩家被传送到新世界;内部的
InstanceProtector应用 EternalTD 的保护规则。
模板要求
复制会在写入任何内容之前进行校验,出现以下情况时模板会被拒绝:
- 它不是目录,或者它是一个符号链接
- 它的根目录下没有
level.dat - 它内部的任何位置包含符号链接
- 它包含名为
.eternaltd-instance的文件(该名称是保留的,见下文)
复制失败会被回滚,并告知玩家关卡未能启动。
实例归属标记
每个克隆都会在其根目录写入一个 .eternaltd-instance 文件,记录模板名称和实例名称。EternalTD 只会删除标记与它认为正在删除的实例相匹配的世界文件夹。这道防线可以避免过期的名称冲突、手工创建的文件夹或不匹配的会话删掉并非由 EternalTD 创建的世界 —— 如果标记缺失或不匹配,该文件夹会被原样保留,并改为记录一条警告。
因崩溃而残留的过期实例会在关卡加载时被清理,而且仅限于携带有效标记且当前未被加载的文件夹。
会话启动校验
只有以下条件全部成立,会话才会变得可游玩。任何一项失败都会中止本局、恢复玩家状态(物品栏、游戏模式、飞行、记分板)并删除临时世界:
levelLocations、startLocation和endLocation全部存在且非空 —— 否则提示 "This level doesn't have a complete play area, start, and end configuration!"- 游戏区域本身能解析成有效网格 —— 否则提示 "This level's play area is invalid!"
- 每个起点方格都同时拥有通往某个终点方格的地面路径和空中路径 —— 否则提示 "This level does not have complete land and air paths from every start to an end!"
当会话结束时:
- 克隆世界中剩余的所有玩家都会被传送回
config.yml中的出生位置,如果未配置出生位置则会被踢出。 - 该世界的保护规则会被移除,所有剩余的塔都会被拆除(在移除过程中抛出异常的塔会被记录并跳过,而不会导致其余的塔无法处理)。
- 克隆世界会被卸载并从磁盘删除(
TemporaryWorldManager.permanentlyDeleteWorld),旧版布局和 Paper 26.1+ 迁移后的布局都会处理。 - 结束操作是幂等的 —— 第二次
end()(例如/etd quit之后紧接着/etd hub)不会产生任何效果。
实例保护规则
在关卡活跃期间,克隆世界会应用以下规则:
- 禁用爆炸
- 禁用液体流动
- 禁用鞘翅
- 阻止切换飞行
- 阻止友军伤害
- 阻止原版生物生成
地图创作工作流程
当前的地图创作流程使用游戏内工具:
- 在
plugins/EternalTD/worlds/<worldName>/下放置一个模板世界文件夹。 - 在
plugins/EternalTD/levels/中创建或下载一个匹配的关卡 YAML。 - 运行
/etd reload并手动加入关卡世界(或在单人模式下打开以进行设置)。 - 使用
/etd selectfloor,并通过右键/左键点击两个角落来标记游戏区域,或使用/etd selectfloorcoordinates <x1> <y1> <z1> <x2> <y2> <z2>直接提供它们。 - 运行
/etd showselection <level>以确认选择看起来正确。 - 运行
/etd register <level>以清除选择。请注意,在当前版本中,register和showselection都不会实际持久化地板区域 —— 本应保存levelLocations的辅助方法(LevelsConfigFields#addLevelLocations)已定义但从未被任何命令调用。如果尚未被下载的包填充,目前你必须手动将levelLocations写入关卡 YAML 中。 - 站在一个起始生成瓦片上并运行
/etd register <level> start。对每个起始瓦片重复此操作(此命令会持久化到startLocation)。 - 站在一个终点瓦片上并运行
/etd register <level> end。对每个终点瓦片重复此操作(此命令会持久化到endLocation)。 - 再次重新加载,并通过 NPC 菜单或
/etd join <level>加入关卡进行测试。
选择命令使用以下公式生成网格方格:
size = abs(corner1 - corner2 + 1) / 3
如果方格的顶部方块为空气(或地板方块可穿过),则会被跳过 —— 因此地板方块必须是实心的,方格才会被注册为可玩。
路径验证
EternalTD 会为每个起始瓦片缓存通往任一终点瓦片的最省 A* 路径 —— 一条地面路径和一条空中路径。该缓存会在会话开始时、每次放置塔时以及每次出售塔时重建。
Air 敌人使用那条单独的空中路径,该路径完全忽略塔的阻挡,并改为遵循空中偏移量(配置路径上方 4 格)。
该缓存同时也是判断关卡是否可游玩的关口:如果任何起始瓦片缺少地面路径或空中路径,会话会在加入时就被拒绝,而不是启动一局敌人无法走完的对局。放置塔时会运行同样的检查,因此会封死某个起始瓦片的塔会被拒绝,金币不会被消耗。
未位于其 3x3 网格方格中心的起点或终点位置会被丢弃并在控制台记录警告,这通常就是关卡报告路径集不完整的原因。
NPC 与关卡菜单
plugins/EternalTD/npcs/ 中的 NPC 配置将村民 NPC 与一个或多个关卡链接起来。右键点击 NPC 会打开一个 9 格物品栏,将每个关卡列为一片绿色染色玻璃板,并标注关卡名称和描述。
| 字段 | 类型 | 默认值 | 备注 |
|---|---|---|---|
isEnabled | bool | true | 已禁用的 NPC 会被跳过 |
levelIDs | string list | 必填 | 此 NPC 提供的关卡文件名 |
location | string | null | 标准 worldName,x,y,z,yaw,pitch 格式的生成位置 |
name | string | "Default Name" | NPC 显示名称 |
difficulty | string | "Difficulty: Not Set" | 显示在 NPC 上方的难度标签 |
disguise | string | null | LibsDisguises 描述符(例如 custom:etd_tutorial_npc) |
customDisguiseData | string | null | 额外的 LibsDisguises 命令数据 —— 通常是较长的玩家皮肤字符串 |
村民生成时具有无敌、AI 禁用、持久化属性,并带有 EternalTD 的 NPC 命名空间键标签。如果安装了 LibsDisguises,并且同时设置了 disguise 和 customDisguiseData,则村民会在生成时被伪装。
带有 difficulty 标签的漂浮盔甲架会生成在 NPC 上方 2.3 格处。
出生点行为
DefaultConfig 控制如何在大厅世界中管理玩家:
setupDone—— 跟踪首次设置引导是否已完成的标志(默认false)。spawnLocations—— 默认为etd_spawn,0,65,0,0,0。仅在etd_spawn世界存在时使用。manageSpawn—— 默认为true。启用时,加入的玩家会在登录后 1 刻被传送到出生位置。playerGuide—— 游戏内指南书的文本。nightbreak.autoDownloadPluginUpdates—— 共享的 MagmaCore 设置(默认false)。启用时,插件和内容更新会在启动时自动下载。
当 manageSpawn 为 true 且出生世界已加载时,每个加入服务器的玩家都会被传送到 spawnLocations。在 EternalTD 仍在初始化期间就已连接的玩家,会在初始化完成后被一并扫描并传送到同一出生点,除非他们已经身处大厅世界。