Bases secretas y gimnasios de jugador
MCE incluye una porción de interior construible para un jugador: el jugador puede entrar a una sala instanciada de su propiedad, decorarla desde un catálogo, ejecutar una Battle Tower infinita, revanchar a los líderes de gimnasio derrotados, e incluso montar su propio gimnasio personalizado con un equipo de líder y una medalla personalizada. Todo aquí es local — viaja en el guardado normal sin migración de esquema.
Visitar las bases de otros jugadores, sincronizar los muebles por la red, y la vivienda con autoridad del servidor son trabajo de la capa online. Esta página cubre solo los sistemas para un jugador. Para la versión en red y visitable, consulte la guía de Vivienda del jugador.
Las cuatro funciones se alcanzan desde el menú de pausa en el juego (GameMenuScreen, casos 8-11) — consulte la guía del Sistema de menús para saber cómo se conectan y protegen las entradas.
Descripción general
| Función | Entrada del menú de pausa | Hogar persistente (en el guardado) |
|---|---|---|
| Mi Base (decorar) | Mi Base (caso 10) | GlobalGameData.SecretBase |
| Mi Gimnasio (gimnasio personalizado) | Mi Gimnasio (caso 11) | GlobalGameData.PlayerGym |
| Battle Tower (infinita) | Torre de Batalla (caso 9) | efímera — no se guarda |
| Batalla / revancha de gimnasio | Gimnasio (caso 8) | asset GymDefinition (creado por el autor) |
Mi Base (base secreta)
Flujo de cara al jugador
Seleccionar Mi Base se ramifica según dónde esté parado el jugador (GameMenuScreen.EnterOrDecorateSecretBase):
- En el mundo → la casilla actual se guarda como la ubicación de retorno y el jugador es teletransportado a la escena de interior instanciada mediante los mismos teletransportes
PlayerTeleporterque usan los warps. - Ya dentro del interior → se abre la paleta de decoración dentro del interior (
SecretBaseDecorateController.Open).
El interior es una única sala instanciada. La decoración guardada del jugador se dibuja al cargar y se re-sincroniza cada vez que cambia.
Decorar
SecretBaseDecorateController es un flujo de colocar/quitar controlado por menú (reutiliza DialogManager.ShowChoiceMenu — sin nuevo framework de UI). La decoración se coloca en la casilla que mira el jugador (la misma matemática de casilla-frente que usa el objeto "Base Kit"):
- Colocar — elija una decoración de la paleta del catálogo, luego se coloca en la casilla enfrentada.
- Quitar — se elimina cualquier decoración que cubra la casilla enfrentada (el anclaje reservado del mundo de la base está protegido y nunca puede eliminarse de esta manera).
Toda la legalidad (límites + solapamiento) vive en el núcleo puro SecretBasePlacement — el controlador solo recopila la elección del jugador y llama al gestor.
El modelo de persistencia
Una base secreta es un POCO simple serializable con JsonUtility que viaja en GlobalGameData.sav con cero migración de formato de guardado (el mismo precedente que 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();
}
Bajo el modelo de base única, la celda de anclaje del mundo de la base se persiste como un PlacedDecor con id reservado (SecretBaseManager.BaseAnchorDecorId) dentro de esa misma lista, así el anclaje viaja en la persistencia existente sin nuevos campos de guardado.
El punto de unión en tiempo de ejecución
SecretBaseManager es un puente POCO simple entre una interacción de mundo/menú y el núcleo verificado SecretBasePlacement más el guardado. No posee ninguna de las reglas de colocación — delega cada decisión de legalidad al núcleo puro y solo agrega dos responsabilidades que el núcleo deja deliberadamente fuera: el predicado de transitabilidad y la persistencia en GlobalGameData.SecretBase.
| Miembro | Propósito |
|---|---|
TryPlaceBase(Vector3Int cell, SecretBaseData data) | Anclar una base en una celda del mundo (valida + persiste) |
RemoveBase(Vector3Int cell) | Limpiar el campo de guardado de base única |
AddDecor(string decorId, Vector3Int tile, int rotation = 0) | Resolver contra Catalog, validar contra InteriorBounds, anexar al guardado |
RemoveDecorAt(Vector3Int tile) | Eliminar la decoración que cubre una casilla (rechaza el anclaje) |
MoveDecor(Vector3Int from, Vector3Int to) | Quitar-y-recolocar; revierte ante un objetivo inválido |
InteriorBounds (RectInt) | La única regla de colocabilidad que usa el interior |
Catalog (DecorCatalog) | El resolvedor de id → prefab/sprite/footprint |
OnBasePlaced / OnBaseRemoved / OnDecorChanged | Eventos que escuchan los visuales del mundo/interior |
Configurarlo como desarrollador
Los sistemas están completos a nivel de código; conectar las escenas y assets es tarea del desarrollador. Nada aquí es inventado — cada tipo de abajo es una clase real bajo Assets/OpenMon/Core/Runtime/World/.
1. Crear un catálogo de decoración
Cree un asset DecorCatalog (menú MCE ▸ World ▸ Decor Catalog). Cada DecorEntry es un contenedor serializable simple:
[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;
}
El catálogo es un resolvedor de sandbox: cada entrada está siempre disponible — no hay banderas de desbloqueo, economía ni gating en esta porción. La búsqueda es O(1) via TryGet(decorId, out entry).
2. Construir la escena de interior
Agregue un SecretBaseInteriorLoader a la escena de interior. Es el único MonoBehaviour que posee la sala: construye un SecretBaseManager en tiempo de ejecución enlazado al GlobalGameData vivo, le entrega los puntos de unión del interior (el DecorCatalog y el rectángulo InteriorBounds), lo publica en SecretBaseRegistry y SecretBaseInteriorSession, luego instancia un prefab por cada PlacedDecor y re-sincroniza ante OnDecorChanged.
Campos serializados a asignar:
| Campo | Significado |
|---|---|
catalog | El DecorCatalog entregado al gestor |
interiorBounds | Rectángulo de la sala, en las coordenadas de casilla de esta escena (por defecto 8×8) |
grid | Grid de escena para el mapeo casilla→mundo (recae en coordenadas de identidad) |
decorContainer | Padre para la decoración instanciada (por defecto el transform del loader) |
arrivalMarker | Marcador de autoría de dónde llega el jugador |
El loader implementa IPlayerDataReceiver porque PlayerDataInstaller enlaza GlobalGameData con .WhenInjectedInto<IPlayerDataReceiver>(). Sin el marcador, el [Inject] se omite en silencio y el interior se renderiza vacío en tiempo de ejecución aunque todas las pruebas offline pasen.
3. Conectar la entrada del menú de pausa
En el prefab GameMenu (GameMenuScreen), asigne:
secretBaseInteriorScene— elSceneInfoAssetde la plantilla de interior.secretBaseInteriorSpawn— la casilla a la que llega el jugador (debería estar dentro deinteriorBounds).
Hasta que se asigne secretBaseInteriorScene, el paso de entrada de Mi Base es un no-op seguro. La casilla de retorno se captura en GlobalGameData.SecretBaseReturn y viaja de ida y vuelta a través del guardado, así la salida dinámica del interior (ExitSecretBaseInterior) devuelve al jugador exactamente donde lo dejó.
4. (Opcional) Mostrar la base en el mundo
SecretBaseSpawner convierte OnBasePlaced / OnBaseRemoved en un prefab visible. Enlace el gestor en tiempo de ejecución con SetManager(...) (que también lo registra en SecretBaseRegistry para que el objeto "Base Kit" pueda resolverlo), y cree baseVisualPrefab. Bajo el modelo de base única, una nueva colocación primero despawnea cualquier visual de base previo. El spawn/despawn/mapeo de cuadrícula son puntos de unión inyectables (SetSpawnFactory, SetDespawner, SetCellToWorld) para que el flujo sea testeable en edit-mode sin una escena viva.
Mi Gimnasio (gimnasio propio del jugador)
Mi Gimnasio (caso 11) es la variante de gimnasio del flujo de la base. Reutiliza el motor de decoración exacto — una Room SecretBaseData validada por el mismo núcleo puro SecretBasePlacement — y agrega los datos específicos del gimnasio que una base no conoce.
Aprovisionamiento en la primera entrada
En la primera entrada desde el mundo (GameMenuScreen.EnsureGymEstablished), el gimnasio se establece con valores por defecto razonables:
- El gimnasio se marca como
Established. - El equipo del líder recae en el equipo actual del jugador (cada ranura de roster ocupada).
- Se asigna un id de medalla temporal para que un retador tenga algo que ganar.
Esto es idempotente: un gimnasio ya establecido se deja tal cual, excepto que un equipo de líder aún vacío recibe el valor por defecto.
El modelo persistente
[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 = "";
}
Todo aterriza en los campos de guardado existentes (GlobalGameData.PlayerGym / GlobalGameData.EarnedGymBadges), así viaja de ida y vuelta sin migración.
El punto de unión en tiempo de ejecución
GymManager es para el gimnasio lo que SecretBaseManager es para la base — un POCO simple que delega toda la legalidad de decoración al núcleo compartido y agrega los aspectos exclusivos del gimnasio:
| Miembro | Propósito |
|---|---|
EstablishGym(string gymName = null) | Crear el gimnasio (idempotente) |
HasGym() | ¿Ha creado el jugador su gimnasio? |
SetLeaderSlots(IEnumerable<int> slots, int rosterSize) | Reemplazar el equipo del líder (saneado, sin duplicados, en rango) |
HasLeaderTeam(int rosterSize) | ¿Es válida al menos una ranura de líder? |
SetBadge(string badgeId, string badgeDisplayName = null) | Establecer la medalla personalizada que ganan los retadores |
SetDialogs(string introKey, string victoryKey) | Establecer las claves de diálogo de la batalla del líder |
AwardEarnedBadge(string badgeId) | Otorgar la medalla a un retador una vez |
CanChallenge(string ownerId, string challengerId) | Regla pura: un dueño nunca puede ganar la medalla de su propio gimnasio |
AddDecor / RemoveDecorAt / MoveDecor | La misma API de decoración que la base, sobre la Room del gimnasio |
Construir la escena de interior del gimnasio refleja exactamente a la base: agregue un GymInteriorLoader (también un IPlayerDataReceiver), asigne su catalog e interiorBounds, y establezca gymInteriorScene + gymInteriorSpawn en el prefab GameMenu. El interior del gimnasio reutiliza la salida dinámica del interior de la base, así guarda la ubicación de retorno de la misma manera (los dos interiores nunca se entran simultáneamente).
La instantánea congelada del líder que desafiaría un visitante remoto (para que el dueño pueda estar desconectado) es la porción del servidor — el modelo local referencia el equipo del líder por ranura de roster, que es todo lo que necesita el gimnasio personalizado del lado del cliente.
Batallas y revanchas de gimnasio
Gimnasio (caso 8) lanza una batalla de práctica/revancha contra el líder de gimnasio configurado en el menú. Obtiene su contexto de lanzamiento exactamente como una pelea de entrenador normal del mundo (SingleTrainerFight) y vuelve a entrar a la cuadrícula del mundo al ganar.
Un gimnasio se crea como un ScriptableObject GymDefinition (menú MCE ▸ Gyms ▸ Gym Definition) que agrega las primitivas de las que se compone un gimnasio:
| Campo | Propósito |
|---|---|
LeaderNameLocalizationKey | Clave de localización del nombre visible del líder |
LeaderRoster | El roster de batalla del líder (necesita ≥ 1 monstruo no nulo) |
LeaderCharacter (CharacterData) | Requerido — la batalla lo dereferencia para el sprite frontal del entrenador |
LeaderBag (Bag) | Requerido — el módulo de objetos clona la bolsa del entrenador enemigo |
BadgeReward (Badge) | Otorgada cuando el líder es derrotado |
TrainerRosters | Subentrenadores opcionales a limpiar antes del líder |
IntroDialogKey / DefeatDialogKey / PostBattleDialogKey | Claves de localización de diálogo durante el flujo de batalla |
LeaderCommandGraph | Flujo de batalla del líder generado (desde el Gym Builder) |
GymOrder | Sugerencia de orden opcional (1 = primer gimnasio) |
Asigne el GymDefinition a GameMenuScreen.menuGymDefinition en el prefab del menú. Cuando la ranura (o su LeaderRoster) no está asignada, la entrada es un no-op seguro.
Battle Tower
Torre de Batalla (caso 9) genera una Battle Tower desechable, sin escena bajo demanda via EphemeralTowerLauncher.Launch(diContainer, playerCharacter). Levanta un TowerController + TowerRewardPayout en un GameObject nuevo, ejecuta el bucle infinito, y desmonta el GameObject en el momento en que la partida alcanza cualquier estado terminal (derrota, aborto o un rechazo de equipo ilegal).
Propiedades clave:
- Efímera — deliberadamente no refleja el estado de la partida en
GlobalGameData.TowerRun, así una partida lanzada desde el menú nunca se filtra a un guardado. Vive y muere con su GameObject. - Recompensas reales — el dinero igual se paga: el pago se enlaza directamente a la billetera real del jugador (
player.PlayerBag), coincidiendo con la ruta autorizadaStartTowerRun. - Batalla individual infinita —
TowerControllerrelanza la siguiente batalla desde el propio callback de fin de cada batalla y continúa hasta una derrota, disparando eventos de finalización de set en cada límite de set.
Los oponentes son anónimos — generados a partir de las referencias del roster del jugador, no llevan CharacterData ni Bag. El sistema de batalla fue endurecido para tolerar esto (consulte Sistema de batalla ▸ entrenadores anónimos).
Balance de fábrica
TowerBalanceDefaults provee un conjunto de reglas y una curva de recompensa jugables de reserva, para que una torre nueva no esté vacía. Un diseñador sobreescribe estos en TowerController (serializado) o via SetRules:
- Conjunto de reglas — tope de nivel 50, autonivelado al tope activado, tamaño de equipo 3, batallas individuales, Cláusula de Especies activada (sin duplicados), legendarios prohibidos, longitud de set 7.
- Curva de recompensa (racha → dinero) —
0 → 500,7 → 1500,21 → 5000,49 → 15000.
Los números por defecto son marcadores de mejor criterio — ajústelos contra su tabla de precios de tienda antes de lanzar.