Extending Monster Data
This page documents internal engine code available exclusively to Source tier licensees.
MCE's monster data model is designed for extension. This page covers the key data structures, how to add new monsters with custom forms, how the evolution system works, and how to safely extend the model without breaking existing save files.
Data Model Overview
MonsterEntry (ScriptableObject -- one per species)
├── DexNumber
├── AvailableForms: List<Form>
└── DataByForm: List<DataByFormEntry> ← one entry per form
├── Graphics (sprites, materials, icons, world sprites)
├── Types (FirstType, SecondType)
├── Dex (description, species, height, weight, cry)
├── Abilities (normal + hidden)
├── Training (EVYield, CatchRate, BaseFriendship, BaseExperience, GrowthRate)
├── Breeding (EggGroups, EggCycles, BreedingData, gender ratios)
├── Wild (run chance, held items)
├── Moves (by level, on evolution, egg moves, other/TM)
├── BaseStats: Dictionary<Stat, byte>
├── Evolutions: List<EvolutionData> ← polymorphic
├── MegaEvolutions: Dictionary<Item, Form>
└── Flags (IsLegendary, IsMythical, IsUltraBeast, CanLearnAnyMove)
At runtime, a MonsterInstance wraps a MonsterEntry with instance-specific state: level, IVs, EVs, current HP, nickname, held item, friendship, and the active form.
Adding a New Monster
Step 1: Create the MonsterEntry Asset
Use the menu MCE > Monster Database > Monster (or right-click Create > MCE > Monster Database > Monster) to create a new MonsterEntry ScriptableObject.
Set these fields:
- DexNumber -- The monster's index in the dex.
- LocalizableName -- Localization key (auto-filled by the "Auto" button based on the asset name, following the pattern
Monsters/{name}). - AvailableForms -- At minimum, add one
Formasset (typically "Normal"). The engine reads shiny variants automatically from each form'sShinyVersion.
Step 2: Configure DataByFormEntry
For each form in AvailableForms, a DataByFormEntry is stored in the DataByForm list. Each entry contains:
Graphics
| Field | Type | Purpose |
|---|---|---|
Front / Back | Material | Battle sprite materials (female/default) |
FrontMale / BackMale | Material | Male override materials (shown if HasMaleMaterialOverride) |
FrontShiny / BackShiny | Material | Shiny variant materials |
Icon / IconShiny / IconMale / IconShinyMale | Sprite | Menu/party icons |
EggMaterial / EggIcon | Material / Sprite | Egg visuals |
WorldIdleSprite* / WorldWalkingSprite* | Sprite / List<Sprite> | Overworld follower sprites (4 directions, with male/shiny variants) |
Types and Dex
public MonsterType FirstType; // Primary type
public MonsterType SecondType; // Secondary type (can be null/None)
public string DexDescriptionKey; // Localization key for the dex entry
public MonsterSpecies Species; // e.g., "Seed Monster"
public float Height; // In meters
public float Weight; // In kilograms
public AudioReference Cry; // Sound effect
Abilities
public List<Ability> Abilities; // Normal abilities (1-2)
public List<Ability> HiddenAbilities; // Hidden abilities
Training
public List<StatByteValuePair> EVYield; // EV points awarded on defeat
public byte CatchRate; // 0-255
public byte BaseFriendship; // Starting friendship value
public uint BaseExperience; // Base XP yield
public GrowthRate GrowthRate; // XP curve type
Breeding
public List<EggGroup> EggGroups; // Compatible breeding groups
public byte EggCycles; // Steps to hatch
public List<BreedingData> BreedingData; // What species can hatch from eggs
public List<PrehatchCallback> PrehatchCallbacks; // Pre-hatch form changes
public bool HasBinaryGender;
public float FemaleRatio; // 0.0 to 1.0
Base Stats
public SerializableDictionary<Stat, byte> BaseStats; // HP, Atk, Def, SpA, SpD, Spe
public SerializableDictionary<Stat, uint> StatLimits; // Hard caps (e.g., Shedinja HP=1)
Move Assignments
Moves are organized into four categories:
| Field | When Learned |
|---|---|
MovesByLevel (List<MoveLevelPair>) | On reaching a specific level |
OnEvolutionMoves (List<Move>) | Immediately after evolving into this form |
EggMoves (List<Move>) | Inherited through breeding |
OtherLearnMoves (List<Move>) | TMs, tutors, or other methods |
The CanLearnAnyMove flag bypasses the OtherLearnMoves list entirely, allowing the monster to learn any move in the database (used for special monsters).
The Inspector validates moves in real-time and flags duplicates across categories.
Step 3: Use the Monster Creation Helper
Click the Open Helper button on any DataByFormEntry to launch the MCE > Monster Creation Helper editor window, which provides a streamlined workflow for filling in all required data.
Step 4: Register in the Database
Add the new MonsterEntry to the MonsterDatabaseInstance asset so it appears in the dex and is available to all engine systems.
The Form System
Forms allow a single species to have multiple visual and mechanical variants (e.g., regional forms, Mega Evolutions, gender differences).
Each Form is a ScriptableObject with:
- A name identifier
- A
HasShinyVersionflag and a reference to itsShinyVersionform - An
IsShinyflag on the shiny variant
MonsterEntry.AvailableForms lists all non-shiny forms. Shiny forms are automatically derived via AvailableFormsWithShinnies.
Adding a New Form
- Create a
FormScriptableObject asset. - If the form has a shiny variant, create a second
Formasset and link them viaShinyVersion/IsShiny. - Add the normal form to the monster's
AvailableFormslist. - A new
DataByFormEntryslot will appear inDataByForm-- fill in the form-specific data (types, stats, sprites, moves, evolutions).
The Evolution System
Architecture
Evolution is driven by a polymorphic command pattern:
EvolutionData (abstract base)
└── TargetSpeciesAndFormEvolutionData (abstract -- adds TargetSpecies/TargetForm/KeepShinyIfItIs)
├── EvolveByLevel
├── EvolveByFriendship
│ └── EvolveByFriendshipAtSpecificTime
├── EvolveOnItemUse
├── EvolveWhenTraded
├── EvolveOnLevelUpHoldingItem
├── EvolveOnLevelUpWhenAMoveHasBeenLearnt
└── ... (30+ built-in evolution types)
Each DataByFormEntry has a List<EvolutionData> Evolutions field using [SerializeReference] for polymorphic serialization. This means you can mix and match different evolution types in the Inspector.
EvolutionData Abstract API
Every evolution subclass must implement these methods:
public abstract class EvolutionData
{
// Check triggers -- return true if the condition is met
public abstract bool CanEvolveAfterLevelUp(MonsterInstance monster,
DayMoment currentTime,
PlayerCharacter playerCharacter,
out bool consumeHeldItem);
public abstract bool CanEvolveAfterBattleThroughExtraData(MonsterInstance monster,
DayMoment currentTime,
out bool consumeHeldItem);
public abstract bool CanEvolveWhenUsingItem(MonsterInstance monster,
DayMoment currentTime,
Item item,
PlayerCharacter playerCharacter,
out bool consumeHeldItem);
public abstract bool CanEvolveWhenTrading(MonsterInstance monster,
DayMoment currentTime,
MonsterInstance otherMonster,
PlayerCharacter playerCharacter,
out bool consumeHeldItem);
// What it evolves into
public abstract (MonsterEntry, Form) GetTargetEvolution(MonsterInstance monster, DayMoment currentTime);
// Post-evolution hook (optional)
public virtual void AfterEvolutionCallback(MonsterInstance evolvedMonster, ...);
// Dex display data
public abstract List<DexMonsterRelationshipData> GetDexRelationships(...);
// Deep copy
public abstract EvolutionData Clone();
}
Built-In Evolution Types
MCE ships with 30+ evolution types. Here are the most commonly used:
| Class | Trigger |
|---|---|
EvolveByLevel | Monster reaches a target level |
EvolveByLevelAtSpecificTime | Level + time of day |
EvolveByLevelWhenSpecificGender | Level + gender check |
EvolveByFriendship | Friendship threshold reached |
EvolveByFriendshipAtSpecificTime | Friendship + time of day |
EvolveByFriendshipWithMoveOfType | Friendship + knows a move of a certain type |
EvolveOnItemUse | Player uses a specific item on the monster |
EvolveOnItemUseAtSpecificTime | Item use + time of day |
EvolveOnItemUseOnSpecificGender | Item use + gender check |
EvolveOnLevelUpHoldingItem | Level up while holding a specific item |
EvolveOnLevelUpWhenAMoveHasBeenLearnt | Level up + knows a specific move |
EvolveOnLevelUpWithOtherMonInParty | Level up + another species is in the party |
EvolveWhenTraded | Traded to another player |
EvolveWhenTradedHoldingItem | Traded while holding a specific item |
EvolveWhenTradedWithMonster | Traded for a specific species |
EvolveByCriticalHitCounter | Lands N critical hits in a single battle |
EvolveByDefeatingRivals | Defeats N monsters of a specific species |
EvolveByUsingMoveXTimes | Uses a specific move N times |
EvolveByEvolutionCounter | Evolves after a counter threshold |
EvolveByLevelWithSceneTag | Level up in a scene with a specific tag |
EvolveToRandomSpeciesByLevel | Random target species on level up |
NincadaEvolution | Special dual-evolution (creates a second monster) |
Creating a Custom Evolution Type
Here is an example of a "evolve when HP is below 50%" evolution:
using System;
using System.Collections.Generic;
using UnityEngine;
using OpenMon.MCE.Characters;
using OpenMon.MCE.GameFlow;
using OpenMon.MCE.MonsterDatabase.Items;
using OpenMon.MCE.MonsterDex;
using OpenMon.MCE.UI.Dex;
using OpenMon.Foundation.Localization;
namespace OpenMon.MCE.Monster.Evolution
{
[Serializable]
public class EvolveWhenLowHP : TargetSpeciesAndFormEvolutionData
{
[SerializeField]
[Range(0f, 1f)]
public float HPThreshold = 0.5f;
public override bool CanEvolveAfterLevelUp(MonsterInstance monster,
DayMoment currentTime,
PlayerCharacter playerCharacter,
out bool consumeHeldItem)
{
consumeHeldItem = false;
float hpRatio = (float)monster.CurrentHP / monster.GetMaxHP();
return monster.AllowEvolution(this) && hpRatio <= HPThreshold;
}
// Return false for triggers this evolution does not use
public override bool CanEvolveAfterBattleThroughExtraData(MonsterInstance m,
DayMoment t, out bool c) { c = false; return false; }
public override bool CanEvolveWhenUsingItem(MonsterInstance m, DayMoment t,
Item i, PlayerCharacter p, out bool c) { c = false; return false; }
public override bool CanEvolveWhenTrading(MonsterInstance m, DayMoment t,
MonsterInstance o, PlayerCharacter p, out bool c) { c = false; return false; }
public override List<DexMonsterRelationshipData> GetDexRelationships(
MonsterDexEntry entry, FormDexEntry formEntry, MonsterGender gender,
ILocalizer localizer)
{
return new List<DexMonsterRelationshipData>
{
new()
{
Species = TargetSpecies,
Form = TargetForm,
Gender = gender,
Mode = DexMonsterRelationshipData.RelationShipDisplayType.Text,
Text = "HP < " + (HPThreshold * 100) + "%",
LocalizableDescriptionKey = "Dex/Evolutions/LowHP"
}
};
}
public override EvolutionData Clone() =>
new EvolveWhenLowHP
{
HPThreshold = HPThreshold,
TargetSpecies = TargetSpecies,
TargetForm = TargetForm,
KeepShinyIfItIs = KeepShinyIfItIs
};
}
}
Because Evolutions uses [SerializeReference], your new subclass will automatically appear in the Inspector's type picker dropdown when adding evolution entries.
How Evolution is Triggered at Runtime
The EvolutionManager ScriptableObject orchestrates evolution at runtime:
- After a level-up,
TriggerEvolutionsAfterLevelUp()iterates all party monsters withNeedsLevelUpEvolutionCheckset totrue. - For each monster, it iterates
FormData.Evolutionsand callsCanEvolveAfterLevelUp(). - On the first match, it calls
TriggerEvolution(), which:- Loads the evolution scene (fade to black).
- Plays the evolution animation (can be cancelled by the player).
- If not cancelled, calls
AfterEvolutionCallback()for post-evolution logic.
- After battle,
TriggerEvolutionAfterBattleThroughExtraData()handles counter-based evolutions (critical hits, move usage, etc.).
Serialization Considerations
[SerializeField] vs [SerializeReference]
[SerializeField]-- Used for concrete types (stats, sprites, basic data). Unity serializes by value.[SerializeReference]-- Used for polymorphic fields likeList<EvolutionData>,List<BreedingData>, andList<PrehatchCallback>. Unity stores the concrete type name, enabling the Inspector to show a type picker.
Save Migration
Never rename serialized fields without a migration plan. Use [FormerlySerializedAs("OldName")] to preserve data when renaming. MCE already uses this pattern extensively (e.g., FrontShinny was renamed to FrontShiny).
When adding new fields to DataByFormEntry or MonsterInstance:
- Add the field with a sensible default value.
- Existing assets will deserialize with the default -- no data loss.
- Save files use JSON serialization via
MCESavesSerializer. New fields will benull/default when loading old saves. Handle this in your code with null checks or default initialization. - Never remove fields from serialized classes. Mark them
[Obsolete]and ignore them instead.
Extending MonsterEntry
To add a new top-level field to MonsterEntry itself (not per-form data):
- Add the field with
[SerializeField]inMonsterEntry.cs. - Use
[FormerlySerializedAs]if renaming an existing field. - Existing monster assets will load with the default value -- update them in batch using an editor script or the Inspector.
To add new per-form data, add fields to DataByFormEntry following the existing group/section pattern with TriInspector attributes.