メインコンテンツまでスキップ

シークレットベースとプレイヤージム

MCE は、シングルプレイヤーの構築可能な内部空間のスライスを同梱しています:プレイヤーは自分が所有するインスタンス化された部屋に入り、カタログから装飾し、無限のバトルタワーを実行し、倒したジムリーダーと再戦し、さらにリーダーチームとカスタムバッジを備えた自分専用のカスタムジムを立ち上げることができます。ここにあるすべてはローカルです — スキーマ移行なしで通常のセーブに乗ります。

マルチプレイヤー

他のプレイヤーのベースを訪問すること、家具をネットワーク越しに同期すること、サーバー権威のハウジングは、オンラインレイヤーの仕事です。このページはシングルプレイヤーのシステムのみを扱います。ネットワーク化された訪問可能なバージョンについては、プレイヤーハウジングガイドを参照してください。

4つの機能はすべて、ゲーム内のポーズメニュー(GameMenuScreen、case 8〜11)から到達します — エントリがどのように接続・ゲートされているかについては、メニューシステムガイドを参照してください。

概要​

機能ポーズメニューエントリ永続的な保存先(セーブ内)
My Base(装飾)Mi Base(case 10)GlobalGameData.SecretBase
My Gym(カスタムジム)Mi Gimnasio(case 11)GlobalGameData.PlayerGym
Battle Tower(無限)Torre de Batalla(case 9)一時的 — 保存されない
Gym バトル/再戦Gimnasio(case 8)GymDefinition アセット(オーサリング済み)

My Base(シークレットベース)​

プレイヤー向けのフロー​

Mi Base を選択すると、プレイヤーが立っている場所で分岐します(GameMenuScreen.EnterOrDecorateSecretBase):

  • フィールドにいる場合 → 現在のタイルが戻り位置として保存され、ワープが使うのと同じ PlayerTeleporter を介して、プレイヤーがインスタンス化された内部シーンにテレポートされます。
  • すでに内部にいる場合 → 内部の装飾パレットが開きます(SecretBaseDecorateController.Open)。

内部は単一のインスタンス化された部屋です。プレイヤーの保存された装飾は読み込み時に描画され、変更されるたびに再同期されます。

装飾​

SecretBaseDecorateController はメニュー駆動の配置/削除フローです(DialogManager.ShowChoiceMenu を再利用 — 新しい UI フレームワークはありません)。装飾はプレイヤーが向いているタイルに配置されます(「Base Kit」アイテムが使うのと同じ向きタイルの計算):

  • 配置 — カタログパレットから装飾を選ぶと、向いているタイルに配置されます。
  • 削除 — 向いているタイルを覆っている装飾が削除されます(ベースの予約済みワールドアンカーは保護され、この方法で削除されることはありません)。

すべての正当性(境界 + 重なり)は、純粋な SecretBasePlacement コアに存在します — コントローラーはプレイヤーの選択を集めてマネージャーを呼び出すだけです。

永続化モデル​

シークレットベースは、GlobalGameData.sav に乗る、単純で JsonUtility シリアライズ可能な POCO で、セーブ形式の移行はありません(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 は、ワールド/メニューの操作と、検証済みの SecretBasePlacement コアおよびセーブとの間の、単純な POCO のブリッジです。配置ルールは何も所有しません — すべての正当性の判断を純粋なコアに委譲し、コアが意図的に残した2つの責務だけを追加します:歩行可能性の述語と、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 ごとにプレハブを1つインスタンス化し、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 の入場ステップは安全な no-op です。戻りタイルは GlobalGameData.SecretBaseReturn に取得され、セーブを往復するため、内部の動的な出口(ExitSecretBaseInterior)はプレイヤーを離れた場所に正確に戻します。

4.(任意)フィールドにベースを表示する​

SecretBaseSpawner は OnBasePlaced / OnBaseRemoved を可視のプレハブに変えます。ランタイムマネージャーを SetManager(...) でバインドし(これは「Base Kit」アイテムが解決できるよう SecretBaseRegistry にも登録します)、baseVisualPrefab をオーサリングします。単一ベースモデルの下では、新しい配置はまず以前のベースのビジュアルをデスポーンします。スポーン/デスポーン/グリッドマッピングは注入可能なシーム(SetSpawnFactory、SetDespawner、SetCellToWorld)であるため、ライブなシーンなしでフローをエディットモードでテストできます。


My Gym(プレイヤー所有のジム)​

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)少なくとも1つのリーダースロットが有効か?
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 を設定します。ジム内部はベース内部の動的な出口を再利用するため、戻り位置を同じ方法で保存します(2つの内部が同時に入場されることはありません)。

注記

リモートの訪問者が挑戦するであろう凍結されたリーダーのスナップショット(所有者がオフラインでいられるように)はサーバースライスです — ローカルモデルはリーダーチームをロスタースロットで参照します。これがクライアント側のカスタムジムに必要なすべてです。


ジムバトルと再戦​

Gimnasio(case 8)は、メニューで設定されたジムリーダーとの練習/再戦バトルを起動します。通常のフィールドのトレーナー戦(SingleTrainerFight)とまったく同じように起動コンテキストを取得し、勝利時にフィールドグリッドに再入場します。

ジムは GymDefinition ScriptableObject(メニュー MCE ▸ Gyms ▸ Gym Definition)としてオーサリングされ、ジムを構成するプリミティブを集約します:

フィールド目的
LeaderNameLocalizationKeyリーダー表示名のロケーションキー
LeaderRosterリーダーのバトルロスター(1体以上の非 null モンスターが必要)
LeaderCharacter(CharacterData)必須 — バトルがトレーナーの表スプライトのためにこれをデリファレンスする
LeaderBag(Bag)必須 — アイテムモジュールが敵トレーナーのバッグをクローンする
BadgeReward(Badge)リーダーを倒したときに授与
TrainerRostersリーダーの前にクリアする任意のサブトレーナー
IntroDialogKey / DefeatDialogKey / PostBattleDialogKeyバトルフローを通じたダイアログのロケーションキー
LeaderCommandGraph生成されたリーダーバトルフロー(Gym Builder から)
GymOrder任意の順序ヒント(1 = 最初のジム)

メニュープレハブの GameMenuScreen.menuGymDefinition に GymDefinition を割り当てます。スロット(またはその LeaderRoster)が未割り当ての場合、エントリは安全な no-op です。


バトルタワー​

Torre de Batalla(case 9)は、EphemeralTowerLauncher.Launch(diContainer, playerCharacter) を介して、使い捨てでシーンレスなバトルタワーをオンデマンドでスポーンします。新しい GameObject 上に TowerController + TowerRewardPayout を立て、無限ループを実行し、実行が終端状態(敗北、中断、または不正チームの拒否)に達した瞬間に GameObject を破棄します。

主な特性:

  • 一時的 — 実行状態を GlobalGameData.TowerRun に意図的にミラーしないため、メニューから起動された実行がセーブに漏れることはありません。GameObject とともに生き、死にます。
  • 実際の報酬 — お金は依然として支払われます:報酬はプレイヤーの実際のウォレット(player.PlayerBag)に直接バインドされ、オーサリングされた StartTowerRun パスと一致します。
  • 無限の単一バトル — TowerController は各バトルの終了コールバックから次のバトルを再起動し、敗北まで続け、各セット境界でセット完了イベントを発火します。

対戦相手は匿名です — プレイヤーのロスター参照から生成され、CharacterData も Bag も持ちません。バトルシステムはこれに対応できるよう堅牢化されました(バトルシステム ▸ 匿名トレーナーを参照)。

標準のバランス​

TowerBalanceDefaults は、新しいタワーが空にならないよう、プレイ可能なフォールバックのルールセットと報酬カーブを提供します。デザイナーはこれらを TowerController(シリアライズ)で、または SetRules を介してオーバーライドします:

  • ルールセット — レベルキャップ50、キャップまで自動レベルアップ有効、チームサイズ3、単一バトル、種族条項有効(重複なし)、伝説禁止、セット長7。
  • 報酬カーブ(連勝 → お金) — 0 → 500、7 → 1500、21 → 5000、49 → 15000。
注意

デフォルトの数値は最善の判断によるプレースホルダーです — 出荷前に、あなたのショップ価格表に対して調整してください。