Перейти к основному содержимому

Секретные базы и залы игрока

MCE поставляется с одиночным срезом застраиваемых интерьеров: игрок может войти в инстансированную комнату, которой он владеет, украсить её из каталога, пройти бесконечную Боевую башню, провести переигровки с побеждёнными лидерами залов и даже создать собственный пользовательский зал с командой лидера и пользовательским значком. Всё здесь локально — оно едет в обычном сохранении без миграции схемы.

Мультиплеер

Посещение баз других игроков, синхронизация мебели по сети и серверно-авторитетное жильё — это задача онлайн-слоя. Эта страница охватывает только одиночные системы. Для сетевой, посещаемой версии см. руководство Жильё игрока.

Все четыре функции доступны из игрового меню паузы (GameMenuScreen, случаи 8–11) — см. руководство Система меню о том, как записи подключены и ограничены.

Обзор​

ФункцияЗапись в меню паузыПостоянный дом (в сохранении)
Моя База (украшение)Mi Base (случай 10)GlobalGameData.SecretBase
Мой Зал (пользовательский зал)Mi Gimnasio (случай 11)GlobalGameData.PlayerGym
Боевая башня (бесконечная)Torre de Batalla (случай 9)эфемерная — не сохраняется
Бой в зале / переигровкаGimnasio (случай 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();
}

В рамках модели одной базы ячейка-якорь базы в надземном мире сохраняется как PlacedDecor с зарезервированным id (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;
}

Каталог — это песочный разрешитель: каждая запись всегда доступна — в этом срезе нет флагов разблокировки, экономики или ограничений. Поиск O(1) через TryGet(decorId, out entry).

2. Постройте сцену интерьера​

Добавьте SecretBaseInteriorLoader в сцену интерьера. Это единственный MonoBehaviour, владеющий комнатой: он конструирует SecretBaseManager времени выполнения, привязанный к живому GlobalGameData, передаёт ему швы интерьера (DecorCatalog и прямоугольник InteriorBounds), публикует его в SecretBaseRegistry и SecretBaseInteriorSession, затем инстанцирует один префаб на каждый PlacedDecor и повторно синхронизируется при OnDecorChanged.

Сериализуемые поля для назначения:

ПолеЗначение
catalogDecorCatalog, переданный менеджеру
interiorBoundsПрямоугольник комнаты, в тайловых координатах этой сцены (по умолчанию 8×8)
gridСцена Grid для отображения тайл→мир (откатывается к тождественным координатам)
decorContainerРодитель для инстанцированного декора (по умолчанию — transform загрузчика)
arrivalMarkerАвторский маркер того, где прибывает игрок
IPlayerDataReceiver обязателен

Загрузчик реализует IPlayerDataReceiver, потому что PlayerDataInstaller привязывает GlobalGameData с .WhenInjectedInto<IPlayerDataReceiver>(). Без этого маркера [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), так что поток тестируется в edit-mode без живой сцены.


Мой Зал (зал, принадлежащий игроку)​

Mi Gimnasio (случай 11) — это вариант зала потока базы. Он повторно использует ровно тот же движок декора — SecretBaseData Room, проверяемая тем же чистым ядром SecretBasePlacement — и добавляет специфичные для зала данные, о которых база не имеет понятия.

Обеспечение при первом входе​

При первом входе из надземного мира (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 и установите gymInteriorScene + gymInteriorSpawn на префабе GameMenu. Интерьер зала повторно использует динамический выход интерьера базы, так что он сохраняет локацию возврата тем же способом (два интерьера никогда не входятся одновременно).

примечание

Замороженный снимок лидера, который бросил бы вызов удалённый посетитель (чтобы владелец мог быть офлайн), — это серверный срез; локальная модель ссылается на команду лидера по слоту отряда, что и есть всё, что нужно пользовательскому залу на стороне клиента.


Бои в залах и переигровки​

Gimnasio (случай 8) запускает тренировочный бой/переигровку против настроенного в меню лидера зала. Он берёт контекст запуска ровно так же, как обычный бой с тренером в надземном мире (SingleTrainerFight), и повторно входит в сетку надземного мира при победе.

Зал создаётся как ScriptableObject GymDefinition (меню MCE ▸ Gyms ▸ Gym Definition), который агрегирует примитивы, из которых состоит зал:

ПолеНазначение
LeaderNameLocalizationKeyКлюч локализации отображаемого имени лидера
LeaderRosterБоевой отряд лидера (нужен ≥ 1 ненулевой монстр)
LeaderCharacter (CharacterData)Обязательно — бой разыменовывает его для фронтального спрайта тренера
LeaderBag (Bag)Обязательно — модуль предметов клонирует сумку вражеского тренера
BadgeReward (Badge)Награждается при поражении лидера
TrainerRostersОпциональные суб-тренеры, побеждаемые перед лидером
IntroDialogKey / DefeatDialogKey / PostBattleDialogKeyКлючи локализации диалогов на протяжении потока боя
LeaderCommandGraphСгенерированный поток боя с лидером (из Gym Builder)
GymOrderОпциональная подсказка порядка (1 = первый зал)

Назначьте GymDefinition в GameMenuScreen.menuGymDefinition на префабе меню. Когда слот (или его LeaderRoster) не назначен, запись является безопасной пустой операцией.


Боевая башня​

Torre de Batalla (случай 9) спаунит одноразовую, бессценовую Боевую башню по требованию через EphemeralTowerLauncher.Launch(diContainer, playerCharacter). Она создаёт TowerController + TowerRewardPayout на свежем GameObject, запускает бесконечный цикл и сносит GameObject в тот момент, когда забег достигает любого терминального состояния (поражение, прерывание или отклонение недопустимой команды).

Ключевые свойства:

  • Эфемерная — она намеренно не отражает состояние забега в GlobalGameData.TowerRun, так что забег, запущенный из меню, никогда не утекает в сохранение. Она живёт и умирает вместе со своим GameObject.
  • Настоящие награды — деньги всё же выплачиваются: выплата привязана прямо к настоящему кошельку игрока (player.PlayerBag), совпадая с авторским путём StartTowerRun.
  • Бесконечный одиночный бой — TowerController перезапускает следующий бой из собственного колбэка окончания каждого боя и продолжает до поражения, вызывая события завершения сета на каждой границе сета.

Противники анонимны — сгенерированные из ссылок отряда игрока, они не несут ни CharacterData, ни Bag. Боевая система была усилена, чтобы это выдерживать (см. Боевая система ▸ анонимные тренеры).

Баланс из коробки​

TowerBalanceDefaults предоставляет играбельный запасной набор правил и кривую наград, чтобы свежая башня не была пустой. Дизайнер переопределяет их на TowerController (сериализовано) или через SetRules:

  • Набор правил — потолок уровня 50, авто-повышение до потолка включено, размер команды 3, одиночные бои, Species Clause включён (без дубликатов), легендарные запрещены, длина сета 7.
  • Кривая наград (серия → деньги) — 0 → 500, 7 → 1500, 21 → 5000, 49 → 15000.
предупреждение

Числа по умолчанию — это заполнители по наилучшему суждению — настройте их под свою таблицу цен магазина перед выпуском.