跳到主要内容

秘密基地与玩家道馆

MCE 附带一个单人可建造室内切片:玩家可以进入一个自己拥有的实例化房间、从目录中装饰它、运行一个无尽的对战塔、与已击败的道馆馆主复赛,甚至立起自己的完全自定义道馆,配有馆主队伍和自定义徽章。这里的一切都是本地的 —— 它搭乘常规存档,无需 schema 迁移。

多人游戏

访问其他玩家的基地、通过网络同步家具,以及服务器权威的房屋,是在线层的职责。本页仅涵盖单人系统。关于联网的、可访问的版本,参见玩家房屋指南。

全部四个功能都从游戏内暂停菜单进入(GameMenuScreen,case 8–11)—— 关于这些条目如何接线和门控,参见菜单系统指南。

概览​

功能暂停菜单条目持久化归宿(存档中)
我的基地(装饰)Mi Base(case 10)GlobalGameData.SecretBase
我的道馆(自定义道馆)Mi Gimnasio(case 11)GlobalGameData.PlayerGym
对战塔(无尽)Torre de Batalla(case 9)短暂性 —— 不保存
道馆对战 / 复赛Gimnasio(case 8)GymDefinition 资产(已编写)

我的基地(秘密基地)​

面向玩家的流程​

选择 Mi Base 会根据玩家所站的位置分支(GameMenuScreen.EnterOrDecorateSecretBase):

  • 在大地图中 → 当前图块被暂存为返回位置,玩家通过与传送相同的 PlayerTeleporter 被传送进实例化的室内场景。
  • 已在室内 → 打开室内装饰调色板(SecretBaseDecorateController.Open)。

室内是一个单独的实例化房间。玩家保存的装饰在加载时绘制,并在每次变更时重新同步。

装饰​

SecretBaseDecorateController 是一个菜单驱动的放置/移除流程(它复用 DialogManager.ShowChoiceMenu —— 没有新的 UI 框架)。装饰放置在玩家面朝的图块上(与"Base Kit"道具使用的相同面朝图块运算):

  • 放置 —— 从目录调色板挑选一个装饰,然后它被放置在面朝的图块上。
  • 移除 —— 覆盖面朝图块的任何装饰被移除(基地保留的世界锚点受保护,永远无法以此方式删除)。

所有合法性(边界 + 重叠)都存在于纯粹的 SecretBasePlacement 核心中 —— 控制器只收集玩家的选择并调用管理器。

持久化模型​

秘密基地是一个普通的、JsonUtility 可序列化的 POCO,搭乘 GlobalGameData.sav 且无存档格式迁移(与 FollowerData 相同的先例):

[Serializable]
public class PlacedDecor
{
public string DecorId; // opaque id, resolved against a DecorCatalog
public Vector3Int Tile; // footprint origin, in real grid coords
public int FootprintWidth = 1;
public int FootprintHeight = 1;
public int Rotation;
public string CustomData; // opaque per-placement data (variant/colour)
}

[Serializable]
public class SecretBaseData
{
public List<PlacedDecor> PlacedDecor = new();
}

在单基地模型下,基地的大地图锚点单元格作为一个保留 id 的 PlacedDecor(SecretBaseManager.BaseAnchorDecorId)持久化在同一个列表中,因此锚点搭乘现有持久化,无需任何新的存档字段。

运行时接缝​

SecretBaseManager 是一个普通 POCO 桥梁,连接一次世界/菜单交互与已验证的 SecretBasePlacement 核心及存档。它不拥有任何放置规则 —— 它将每一个合法性决定委托给纯粹的核心,只增加了两个核心刻意留出的职责:可行走谓词,以及持久化到 GlobalGameData.SecretBase。

成员用途
TryPlaceBase(Vector3Int cell, SecretBaseData data)在大地图单元格锚定一个基地(校验 + 持久化)
RemoveBase(Vector3Int cell)清除单基地存档字段
AddDecor(string decorId, Vector3Int tile, int rotation = 0)针对 Catalog 解析、针对 InteriorBounds 校验、追加到存档
RemoveDecorAt(Vector3Int tile)移除覆盖某图块的装饰(拒绝锚点)
MoveDecor(Vector3Int from, Vector3Int to)移除后再放置;目标无效时回退
InteriorBounds(RectInt)室内使用的唯一可放置性规则
Catalog(DecorCatalog)id → 预制体/精灵图/占位的解析器
OnBasePlaced / OnBaseRemoved / OnDecorChanged世界/室内视觉监听的事件

作为开发者进行设置​

这些系统在代码上已完成;接好场景和资产是开发者的工作。这里没有任何虚构 —— 下面的每种类型都是 Assets/OpenMon/Core/Runtime/World/ 下的真实类。

1. 编写一个装饰目录​

创建一个 DecorCatalog 资产(菜单 MCE ▸ World ▸ Decor Catalog)。每个 DecorEntry 都是一个普通的可序列化持有者:

[Serializable]
public class DecorEntry
{
public string DecorId; // stable, opaque, unique in the catalog
public string DisplayNameLocKey; // localization key shown in the palette
public GameObject Prefab; // spawned when the interior is populated
public Sprite Icon; // palette/menu icon
public int FootprintWidth = 1;
public int FootprintHeight = 1;
}

该目录是一个沙盒解析器:每个条目始终可用 —— 此切片中没有解锁标记、经济或门控。查找通过 TryGet(decorId, out entry) 以 O(1) 完成。

2. 构建室内场景​

向室内场景添加一个 SecretBaseInteriorLoader。它是拥有该房间的唯一 MonoBehaviour:它构造一个绑定到实时 GlobalGameData 的运行时 SecretBaseManager,向它交付室内接缝(DecorCatalog 和 InteriorBounds 矩形),将其发布到 SecretBaseRegistry 和 SecretBaseInteriorSession,然后为每个 PlacedDecor 实例化一个预制体,并在 OnDecorChanged 时重新同步。

要分配的序列化字段:

字段含义
catalog交给管理器的 DecorCatalog
interiorBounds房间矩形,以此场景的图块坐标表示(默认 8×8)
grid用于图块→世界映射的场景 Grid(回退为恒等坐标)
decorContainer实例化装饰的父级(默认为加载器的 transform)
arrivalMarker玩家到达位置的编写标记
IPlayerDataReceiver 是必需的

加载器实现了 IPlayerDataReceiver,因为 PlayerDataInstaller 用 .WhenInjectedInto<IPlayerDataReceiver>() 绑定 GlobalGameData。没有该标记,[Inject] 会被无声跳过,即便每个离线测试都通过,室内在运行时也会渲染为空。

3. 接好暂停菜单条目​

在 GameMenu 预制体(GameMenuScreen)上,分配:

  • secretBaseInteriorScene —— 室内模板的 SceneInfoAsset。
  • secretBaseInteriorSpawn —— 玩家到达的图块(应位于 interiorBounds 之内)。

在 secretBaseInteriorScene 被分配之前,Mi Base 的进入步骤是一个安全的空操作。返回图块在 GlobalGameData.SecretBaseReturn 上捕获并往返于存档,因此室内的动态出口(ExitSecretBaseInterior)会将玩家带回他们离开时的确切位置。

4.(可选)在大地图中显示基地​

SecretBaseSpawner 将 OnBasePlaced / OnBaseRemoved 变成一个可见的预制体。用 SetManager(...) 绑定运行时管理器(它也会将其注册到 SecretBaseRegistry,使"Base Kit"道具能解析它),并编写 baseVisualPrefab。在单基地模型下,一次新的放置会先移除任何之前的基地视觉。生成/移除/网格映射是可注入的接缝(SetSpawnFactory、SetDespawner、SetCellToWorld),因此该流程无需实时场景即可在编辑模式下测试。


我的道馆(玩家自有道馆)​

Mi Gimnasio(case 11)是基地流程的道馆变体。它复用完全相同的装饰引擎 —— 一个由相同纯粹 SecretBasePlacement 核心校验的 SecretBaseData Room —— 并增加了基地毫无概念的道馆特有数据。

首次进入的置备​

在首次大地图进入时(GameMenuScreen.EnsureGymEstablished),道馆以合理的默认值建立:

  • 道馆被标记为 Established。
  • 馆主队伍默认为玩家当前的队伍(每个已占用的队伍槽位)。
  • 分配一个占位徽章 id,使挑战者有东西可赢取。

这是幂等的:一个已建立的道馆保持原样,除了仍为空的馆主队伍会被设为默认。

持久化模型​

[Serializable]
public class PlayerGymData
{
public bool Established;
public string GymName = "";
public SecretBaseData Room = new(); // reuses the F2 decor engine wholesale
public List<int> LeaderRosterSlots = new();// indices into the player's own Roster
public string BadgeId = ""; // custom badge a challenger earns
public string BadgeDisplayName = "";
public string IntroDialogKey = "";
public string VictoryDialogKey = "";
}

一切都落入现有的存档字段(GlobalGameData.PlayerGym / GlobalGameData.EarnedGymBadges),因此它无需迁移即可往返。

运行时接缝​

GymManager 之于道馆,正如 SecretBaseManager 之于基地 —— 一个普通 POCO,将所有装饰合法性委托给共享核心,并增加道馆专属的关切:

成员用途
EstablishGym(string gymName = null)创建道馆(幂等)
HasGym()玩家是否已创建其道馆?
SetLeaderSlots(IEnumerable<int> slots, int rosterSize)替换馆主队伍(净化、去重、范围内)
HasLeaderTeam(int rosterSize)是否至少有一个馆主槽位有效?
SetBadge(string badgeId, string badgeDisplayName = null)设置挑战者赢取的自定义徽章
SetDialogs(string introKey, string victoryKey)设置馆主战斗的对话键
AwardEarnedBadge(string badgeId)将徽章授予挑战者一次
CanChallenge(string ownerId, string challengerId)纯粹规则:拥有者永远无法赢取自己道馆的徽章
AddDecor / RemoveDecorAt / MoveDecor与基地相同的装饰 API,作用于道馆 Room

构建道馆室内场景与基地完全一致:添加一个 GymInteriorLoader(同样是一个 IPlayerDataReceiver),分配其 catalog 和 interiorBounds,并在 GameMenu 预制体上设置 gymInteriorScene + gymInteriorSpawn。道馆室内复用基地室内的动态出口,因此它以相同方式暂存返回位置(两个室内永远不会同时进入)。

备注

远程访客会挑战的那个冻结的馆主快照(使拥有者能够离线)是服务器切片 —— 本地模型按队伍槽位引用馆主队伍,这就是客户端自定义道馆所需的全部。


道馆对战与复赛​

Gimnasio(case 8)发起一场对抗菜单已配置道馆馆主的练习/复赛战斗。它像一场普通的大地图训练家对战(SingleTrainerFight)一样获取其发起上下文,并在胜利时重新进入大地图网格。

道馆被编写为一个 GymDefinition ScriptableObject(菜单 MCE ▸ Gyms ▸ Gym Definition),它聚合了组成一个道馆的基本要素:

字段用途
LeaderNameLocalizationKey馆主显示名称的 loc 键
LeaderRoster馆主的战斗队伍(需 ≥ 1 只非 null 怪物)
LeaderCharacter(CharacterData)必需 —— 战斗解引用它以获取训练家正面精灵图
LeaderBag(Bag)必需 —— 道具模块克隆敌方训练家的背包
BadgeReward(Badge)击败馆主时授予
TrainerRosters可选的、在馆主之前清除的下级训练家
IntroDialogKey / DefeatDialogKey / PostBattleDialogKey贯穿战斗流程的对话 loc 键
LeaderCommandGraph生成的馆主战斗流程(来自 Gym Builder)
GymOrder可选的排序提示(1 = 第一个道馆)

在菜单预制体中将 GymDefinition 分配到 GameMenuScreen.menuGymDefinition。当该槽位(或其 LeaderRoster)未分配时,该条目是一个安全的空操作。


对战塔​

Torre de Batalla(case 9)通过 EphemeralTowerLauncher.Launch(diContainer, playerCharacter) 按需生成一个一次性、无场景的对战塔。它在一个全新的 GameObject 上立起一个 TowerController + TowerRewardPayout,运行无尽循环,并在挑战到达任何终止状态(落败、中止或非法队伍拒绝)的那一刻拆除该 GameObject。

关键属性:

  • 短暂性 —— 它刻意不将挑战状态镜像到 GlobalGameData.TowerRun,因此从菜单发起的挑战永远不会泄漏进存档。它随其 GameObject 而生灭。
  • 真实奖励 —— 金钱仍会支付:奖励直接绑定到玩家的真实钱包(player.PlayerBag),与已编写的 StartTowerRun 路径一致。
  • 无尽单场战斗 —— TowerController 从每场战斗自身的结束回调重新发起下一场战斗,并持续直到落败,在每个 set 边界触发 set 完成事件。

对手是匿名的 —— 它们从玩家的队伍引用生成,不携带任何 CharacterData 或 Bag。战斗系统已被加固以容忍这一点(参见战斗系统 ▸ 匿名训练家)。

开箱平衡​

TowerBalanceDefaults 提供了一个可玩的回退规则集和奖励曲线,使一个全新的塔不至于是空的。设计师可在 TowerController(序列化)上或通过 SetRules 覆盖这些:

  • 规则集 —— 等级上限 50、开启自动升到上限、队伍大小 3、单场战斗、开启物种条款(无重复)、禁止传说、set 长度 7。
  • 奖励曲线(连胜 → 金钱) —— 0 → 500、7 → 1500、21 → 5000、49 → 15000。
警告

默认数值是最佳判断的占位符 —— 在发布前请针对你的商店价格表进行调优。