Implement Mega Stones as {key: value} pairs (#11684)

* Implement Mega Stones as pairs {key: value}

* Fix Mega Evolution check

* Add constructor guards to Dex getters

* Update config/formats.ts

---------

Co-authored-by: Kris Johnson <11083252+KrisXV@users.noreply.github.com>
This commit is contained in:
André Bastos Dias 2026-01-08 21:59:22 +00:00 committed by GitHub
parent ee77cf98ae
commit 684150d9d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 278 additions and 528 deletions

View File

@ -2899,7 +2899,7 @@ export const Formats: import('../sim/dex-formats').FormatList = [
onValidateSet(set, format, setHas, teamHas) {
if (set.item) {
const item = this.dex.items.get(set.item);
if (item.megaEvolves && !(this.ruleTable.has(`+item:${item.id}`) || this.ruleTable.has(`+pokemontag:mega`))) {
if (item.megaStone && !(this.ruleTable.has(`+item:${item.id}`) || this.ruleTable.has(`+pokemontag:mega`))) {
return [`Mega Evolution is banned.`];
}
if (item.zMove && !(this.ruleTable.has(`+item:${item.id}`))) {
@ -3678,8 +3678,8 @@ export const Formats: import('../sim/dex-formats').FormatList = [
}
const item = this.dex.items.get(set.item);
if (set.item && item.megaStone) {
const megaSpecies = this.dex.species.get(Array.isArray(item.megaStone) ? item.megaStone[0] : item.megaStone);
if (item.megaEvolves?.includes(species.baseSpecies) && megaSpecies.bst > 625) {
const megaSpecies = this.dex.species.get(item.megaStone[species.baseSpecies]);
if (megaSpecies.bst > 625) {
return [
`${set.name || set.species}'s item ${item.name} is banned.`, `(Pok\u00e9mon with a BST higher than 625 are banned)`,
];

File diff suppressed because it is too large Load Diff

View File

@ -13,12 +13,10 @@ export const Items: import('../../../sim/dex-items').ModdedItemDataTable = {
masquerainite: {
name: "Masquerainite",
spritenum: 1,
megaStone: "Masquerain-Mega",
megaEvolves: "Masquerain",
megaStone: { "Masquerain": "Masquerain-Mega" },
itemUser: ["Masquerain"],
onTakeItem(item, source) {
if (item.megaEvolves === source.baseSpecies.baseSpecies) return false;
return true;
return !item.megaStone?.[source.baseSpecies.baseSpecies];
},
num: -1,
gen: 9,
@ -59,12 +57,10 @@ export const Items: import('../../../sim/dex-items').ModdedItemDataTable = {
typhlosionite: {
name: "Typhlosionite",
spritenum: 1,
megaStone: "Typhlosion-Mega",
megaEvolves: "Typhlosion",
megaStone: { "Typhlosion": "Typhlosion-Mega" },
itemUser: ["Typhlosion"],
onTakeItem(item, source) {
if (item.megaEvolves === source.baseSpecies.baseSpecies) return false;
return true;
return !item.megaStone?.[source.baseSpecies.baseSpecies];
},
num: -2,
gen: 9,

View File

@ -92,10 +92,10 @@ export const Rulesets: import('../../../sim/dex-formats').ModdedFormatDataTable
if (["Zacian", "Zamazenta"].includes(species.baseSpecies) && this.toID(set.item).startsWith('rusted')) {
species = this.dex.species.get(set.species + "-Crowned");
}
if (set.item && this.dex.items.get(set.item).megaStone) {
if (set.item) {
const item = this.dex.items.get(set.item);
if (item.megaEvolves === species.baseSpecies) {
species = this.dex.species.get(Array.isArray(item.megaStone) ? item.megaStone[0] : item.megaStone);
if (item.megaStone?.[species.baseSpecies]) {
species = this.dex.species.get(item.megaStone[species.baseSpecies]);
}
}
if (
@ -123,8 +123,8 @@ export const Rulesets: import('../../../sim/dex-formats').ModdedFormatDataTable
}
if (set.item) {
const item = this.dex.items.get(set.item);
if (item.megaEvolves === set.species) {
godSpecies = this.dex.species.get(Array.isArray(item.megaStone) ? item.megaStone[0] : item.megaStone);
if (item.megaStone?.[set.species]) {
godSpecies = this.dex.species.get(item.megaStone[set.species]);
}
if (["Zacian", "Zamazenta"].includes(godSpecies.baseSpecies) && item.id.startsWith('rusted')) {
godSpecies = this.dex.species.get(set.species + "-Crowned");

View File

@ -2,15 +2,15 @@ export const Items: import('../../../sim/dex-items').ModdedItemDataTable = {
slowbronite: {
inherit: true,
onTakeItem(item, source) {
if (item.megaEvolves === source.baseSpecies.name || item.megaStone === source.baseSpecies.name) return false;
return true;
return !item.megaStone || (!item.megaStone[source.baseSpecies.name] &&
!Object.values(item.megaStone).includes(source.baseSpecies.name));
},
},
greninjite: {
inherit: true,
onTakeItem(item, source) {
if (item.megaEvolves === source.baseSpecies.name || item.megaStone === source.baseSpecies.name) return false;
return true;
return !item.megaStone || (!item.megaStone[source.baseSpecies.name] &&
!Object.values(item.megaStone).includes(source.baseSpecies.name));
},
},
zygardite: {

View File

@ -57,23 +57,8 @@ export const Scripts: ModdedBattleScriptsData = {
pokemon.baseMoves.includes(this.battle.toID(altForme.requiredMove)) && !item.zMove) {
return altForme.name;
}
if (Array.isArray(item.megaEvolves)) {
if (!Array.isArray(item.megaStone)) {
throw new Error(`${item.name}#megaEvolves and ${item.name}#megaStone type mismatch`);
}
if (item.megaEvolves.length !== item.megaStone.length) {
throw new Error(`${item.name}#megaEvolves and ${item.name}#megaStone length mismatch`);
}
const index = item.megaEvolves.indexOf(species.name);
if (index < 0) return null;
return item.megaStone[index];
} else {
if (item.megaEvolves === species.name) {
if (Array.isArray(item.megaStone)) throw new Error(`${item.name}#megaEvolves and ${item.name}#megaStone type mismatch`);
return item.megaStone;
}
}
return null;
if (!item.megaStone) return null;
return item.megaStone[species.name];
},
runMegaEvo(pokemon) {
const speciesid = pokemon.canMegaEvo || pokemon.canUltraBurst;

View File

@ -14,11 +14,9 @@ export const Items: import('../../../sim/dex-items').ModdedItemDataTable = {
name: "Flygonite",
spritenum: 111,
itemUser: ["Flygon"],
megaEvolves: "Flygon",
megaStone: "Trapinch",
megaStone: { "Trapinch": "Flygon" },
onTakeItem(item, source) {
if (item.megaEvolves === source.baseSpecies.baseSpecies) return false;
return true;
return !item.megaStone?.[source.baseSpecies.baseSpecies];
},
desc: "If held by a Flygon, this item allows it to Mega Evolve in battle.",
},
@ -36,7 +34,7 @@ export const Items: import('../../../sim/dex-items').ModdedItemDataTable = {
gardevoirite: {
inherit: true,
itemUser: ["Ralts"],
megaEvolves: "Ralts",
megaStone: { "Ralts": "Gardevoir-Mega" },
desc: "If held by a Ralts, this item allows it to Mega Evolve in battle.",
},
// Peary

View File

@ -963,26 +963,11 @@ export const Scripts: ModdedBattleScriptsData = {
pokemon.baseMoves.includes(this.battle.toID(altForme.requiredMove)) && !item.zMove) {
return altForme.name;
}
if (!item.megaStone) return null;
// a hacked-in Megazard X can mega evolve into Megazard Y, but not into Megazard X
if (Array.isArray(item.megaEvolves)) {
if (!Array.isArray(item.megaStone)) {
throw new Error(`${item.name}#megaEvolves and ${item.name}#megaStone type mismatch`);
}
if (item.megaEvolves.length !== item.megaStone.length) {
throw new Error(`${item.name}#megaEvolves and ${item.name}#megaStone length mismatch`);
}
// FIXME: Change to species.name when champions comes
const index = item.megaEvolves.indexOf(species.baseSpecies);
if (index < 0) return null;
return item.megaStone[index];
// FIXME: Change to species.name when champions comes
} else {
if (item.megaEvolves === species.baseSpecies) {
if (Array.isArray(item.megaStone)) throw new Error(`${item.name}#megaEvolves and ${item.name}#megaStone type mismatch`);
return item.megaStone;
}
}
return null;
const megaEvolution = item.megaStone[species.baseSpecies];
return megaEvolution && megaEvolution !== species.name ? megaEvolution : null;
},
// 1 Z per pokemon

View File

@ -385,12 +385,8 @@ export const Scripts: ModdedBattleScriptsData = {
if (pokemon.species.isMega) return null;
const item = pokemon.getItem();
if (item.megaStone) {
if (item.megaStone.includes(pokemon.baseSpecies.name)) return null;
return Array.isArray(item.megaStone) ? item.megaStone[0] : item.megaStone;
} else {
return null;
}
if (!item.megaStone || item.megaStone[pokemon.baseSpecies.name]) return null;
return Object.values(item.megaStone)[0];
},
runMegaEvo(pokemon) {
if (pokemon.species.isMega) return false;

View File

@ -1607,8 +1607,7 @@ export class RandomGen7Teams extends RandomGen8Teams {
if (isMonotype) {
// Prevents Mega Evolutions from breaking the type limits
if (itemData.megaStone) {
const megaSpecies = this.dex.species.get(Array.isArray(itemData.megaStone) ?
itemData.megaStone[0] : itemData.megaStone);
const megaSpecies = this.dex.species.get(Object.values(itemData.megaStone)[0]);
if (types.length > megaSpecies.types.length) types = [species.types[0]];
// Only check the second type because a Mega Evolution should always share the first type with its base forme.
if (megaSpecies.types[1] && types[1] && megaSpecies.types[1] !== types[1]) {

View File

@ -1507,18 +1507,10 @@ export const Rulesets: import('../sim/dex-formats').FormatDataTable = {
typeTable = typeTable.filter(type => species.types.includes(type));
}
const item = this.dex.items.get(set.item);
if (item.megaStone) {
if (Array.isArray(item.megaStone)) {
const index = (item.megaEvolves as string[]).indexOf(species.name);
if (index >= 0) {
species = this.dex.species.get(item.megaStone[index]);
if (item.megaStone?.[species.name]) {
species = this.dex.species.get(item.megaStone[species.name]);
typeTable = typeTable.filter(type => species.types.includes(type));
}
} else {
species = this.dex.species.get(item.megaStone);
typeTable = typeTable.filter(type => species.types.includes(type));
}
}
if (item.id === "ultranecroziumz" && species.baseSpecies === "Necrozma") {
species = this.dex.species.get("Necrozma-Ultra");
typeTable = typeTable.filter(type => species.types.includes(type));
@ -1556,18 +1548,10 @@ export const Rulesets: import('../sim/dex-formats').FormatDataTable = {
}
color = species.color;
const item = this.dex.items.get(set.item);
if (item.megaStone) {
if (Array.isArray(item.megaStone)) {
const index = (item.megaEvolves as string[]).indexOf(species.name);
if (index >= 0) {
species = this.dex.species.get(item.megaStone[index]);
if (item.megaStone?.[species.name]) {
species = this.dex.species.get(item.megaStone[species.name]);
color = species.color;
}
} else {
species = this.dex.species.get(item.megaStone);
color = species.color;
}
}
if (item.id === "ultranecroziumz" && species.baseSpecies === "Necrozma") {
species = this.dex.species.get("Necrozma-Ultra");
color = species.color;
@ -2666,12 +2650,10 @@ export const Rulesets: import('../sim/dex-formats').FormatDataTable = {
) {
species = this.dex.species.get(`${species.baseSpecies}-Crowned`);
}
if (set.item && this.dex.items.get(set.item).megaStone) {
if (set.item) {
const item = this.dex.items.get(set.item);
if (item.megaEvolves?.includes(species.name)) {
species = this.dex.species.get(Array.isArray(item.megaEvolves) ?
(item.megaStone as string[])[item.megaEvolves.indexOf(species.name)] :
item.megaStone as string);
if (item.megaStone?.[species.name]) {
species = this.dex.species.get(item.megaStone[species.name]);
}
}
if (this.ruleTable.isRestrictedSpecies(species) ||
@ -2693,10 +2675,8 @@ export const Rulesets: import('../sim/dex-formats').FormatDataTable = {
}
if (set.item) {
const item = this.dex.items.get(set.item);
if (item.megaEvolves?.includes(set.species)) {
godSpecies = this.dex.species.get(Array.isArray(item.megaEvolves) ?
(item.megaStone as string[])[item.megaEvolves.indexOf(set.species)] :
item.megaStone as string);
if (item.megaStone?.[set.species]) {
godSpecies = this.dex.species.get(item.megaStone[set.species]);
}
if (["Zacian", "Zamazenta"].includes(godSpecies.baseSpecies) && item.id.startsWith('rusted')) {
godSpecies = this.dex.species.get(set.species + "-Crowned");

View File

@ -29,14 +29,23 @@ function getMegaStone(stone: string, mod = 'gen9'): Item | null {
id: move.id,
name: move.name,
fullname: move.name,
megaEvolves: 'Rayquaza',
megaStone: 'Rayquaza-Mega',
megaStone: { 'Rayquaza': 'Rayquaza-Mega' },
exists: true,
// Adding extra values to appease typescript
gen: 6,
num: -1,
effectType: 'Item',
sourceEffect: '',
isBerry: false,
ignoreKlutz: false,
isGem: false,
isPokeball: false,
isPrimalOrb: false,
shortDesc: "",
desc: "",
isNonstandard: null,
noCopy: false,
affectsFainted: false,
} as Item;
} else {
return null;
@ -131,8 +140,8 @@ export const commands: Chat.ChatCommands = {
megaSpecies = dex.species.get(forcedForme);
baseSpecies = dex.species.get(forcedForme.split('-')[0]);
} else {
megaSpecies = dex.species.get(Array.isArray(stone.megaStone) ? stone.megaStone[0] : stone.megaStone);
baseSpecies = dex.species.get(Array.isArray(stone.megaEvolves) ? stone.megaEvolves[0] : stone.megaEvolves);
megaSpecies = dex.species.get(Object.values(stone.megaStone!)[0]);
baseSpecies = dex.species.get(Object.keys(stone.megaStone!)[0]);
}
break;
}
@ -282,8 +291,8 @@ export const commands: Chat.ChatCommands = {
megaSpecies = dex.species.get(forcedForme);
baseSpecies = dex.species.get(forcedForme.split('-')[0]);
} else {
megaSpecies = dex.species.get(Array.isArray(aStone.megaStone) ? aStone.megaStone[0] : aStone.megaStone);
baseSpecies = dex.species.get(Array.isArray(aStone.megaEvolves) ? aStone.megaEvolves[0] : aStone.megaEvolves);
megaSpecies = dex.species.get(Object.values(aStone.megaStone!)[0]);
baseSpecies = dex.species.get(Object.keys(aStone.megaStone!)[0]);
}
break;
}

View File

@ -373,7 +373,7 @@ class SSBSetsHTML extends Chat.JSX.Component<{ target: string }> {
<SSBInnateHTML name={setName} dex={dex} baseDex={baseDex} />
<SSBPokemonHTML species={set.species} dex={dex} baseDex={baseDex} />
{(!Array.isArray(set.item) && item.megaStone) && <SSBPokemonHTML
species={Array.isArray(item.megaStone) ? item.megaStone[0] : item.megaStone} dex={dex} baseDex={baseDex}
species={Object.values(item.megaStone)[0]} dex={dex} baseDex={baseDex}
/>}
{/* keys and Kennedy have an itemless forme change */}
{['Rayquaza'].includes(set.species) && <SSBPokemonHTML species={`${set.species}-Mega`} dex={dex} baseDex={baseDex} />}

View File

@ -134,7 +134,7 @@ export function getSpeciesName(set: PokemonSet, format: Format) {
} else if (species === "Groudon" && item.name === "Red Orb") {
return "Groudon-Primal";
} else if (item.megaStone) {
return Array.isArray(item.megaStone) ? item.megaStone[0] : item.megaStone;
return Object.values(item.megaStone)[0];
} else if (species === "Rayquaza" && moves.includes('Dragon Ascent') && !item.zMove && megaRayquazaPossible) {
return "Rayquaza-Mega";
} else if (species === "Poltchageist-Artisan") { // Babymons from here on out

View File

@ -1871,21 +1871,15 @@ export class BattleActions {
pokemon.baseMoves.includes(toID(altForme.requiredMove)) && !item.zMove) {
return altForme.name;
}
if (!item.megaStone) return null;
// Temporary hardcode until generation shift
if ((species.baseSpecies === "Floette" || species.baseSpecies === "Zygarde") && item.megaEvolves === species.name) {
return item.megaStone as string;
if ((species.baseSpecies === "Floette" || species.baseSpecies === "Zygarde") && item.megaStone[species.name]) {
return item.megaStone[species.name];
}
// a hacked-in Megazard X can mega evolve into Megazard Y, but not into Megazard X
if (Array.isArray(item.megaStone)) {
// FIXME: Change to species.name when champions comes
const index = (item.megaEvolves as string[]).indexOf(species.baseSpecies);
if (index < 0) return null;
return item.megaStone[index];
// FIXME: Change to species.name when champions comes
} else if (item.megaEvolves === species.baseSpecies && item.megaStone !== species.name) {
return item.megaStone;
}
return null;
const megaEvolution = item.megaStone[species.baseSpecies];
return megaEvolution && megaEvolution !== species.name ? megaEvolution : null;
}
canUltraBurst(pokemon: Pokemon) {

View File

@ -85,7 +85,7 @@ export class DexAbilities {
}
getByID(id: ID): Ability {
if (id === '') return EMPTY_ABILITY;
if (id === '' || id === 'constructor') return EMPTY_ABILITY;
let ability = this.abilityCache.get(id);
if (ability) return ability;

View File

@ -665,7 +665,7 @@ export class DexConditions {
}
getByID(id: ID): Condition {
if (id === '') return EMPTY_CONDITION;
if (id === '' || id === 'constructor') return EMPTY_CONDITION;
let condition = this.conditionCache.get(id);
if (condition) return condition;

View File

@ -172,7 +172,7 @@ export class DexNatures {
return this.getByID(toID(name));
}
getByID(id: ID): Nature {
if (id === '') return EMPTY_NATURE;
if (id === '' || id === 'constructor') return EMPTY_NATURE;
let nature = this.natureCache.get(id);
if (nature) return nature;
@ -293,7 +293,7 @@ export class DexTypes {
}
getByID(id: ID): TypeInfo {
if (id === '') return EMPTY_TYPE_INFO;
if (id === '' || id === 'constructor') return EMPTY_TYPE_INFO;
let type = this.typeCache.get(id);
if (type) return type;

View File

@ -43,17 +43,11 @@ export class Item extends BasicEffect implements Readonly<BasicEffect> {
*/
readonly onMemory?: string;
/**
* If this is a mega stone: The name (e.g. Charizard-Mega-X) of the
* forme this allows transformation into.
* If this is a mega stone: A pair (e.g. Charizard: Charizard-Mega-X) of the
* forme this allows transformation from and into.
* undefined, if not a mega stone.
*/
readonly megaStone?: string | string[];
/**
* If this is a mega stone: The name (e.g. Charizard) of the
* forme this allows transformation from.
* undefined, if not a mega stone.
*/
readonly megaEvolves?: string | string[];
readonly megaStone?: { [megaEvolves: string]: string };
/**
* If this is a Z crystal: true if the Z Crystal is generic
* (e.g. Firium Z). If species-specific, the name
@ -116,7 +110,6 @@ export class Item extends BasicEffect implements Readonly<BasicEffect> {
this.onDrive = data.onDrive || undefined;
this.onMemory = data.onMemory || undefined;
this.megaStone = data.megaStone || undefined;
this.megaEvolves = data.megaEvolves || undefined;
this.zMove = data.zMove || undefined;
this.zMoveType = data.zMoveType || undefined;
this.zMoveFrom = data.zMoveFrom || undefined;
@ -176,7 +169,7 @@ export class DexItems {
}
getByID(id: ID): Item {
if (id === '') return EMPTY_ITEM;
if (id === '' || id === 'constructor') return EMPTY_ITEM;
let item = this.itemCache.get(id);
if (item) return item;
if (this.dex.getAlias(id)) {

View File

@ -621,7 +621,7 @@ export class DexMoves {
}
getByID(id: ID): Move {
if (id === '') return EMPTY_MOVE;
if (id === '' || id === 'constructor') return EMPTY_MOVE;
let move = this.moveCache.get(id);
if (move) return move;
if (this.dex.getAlias(id)) {

View File

@ -436,7 +436,7 @@ export class DexSpecies {
}
getByID(id: ID): Species {
if (id === '') return EMPTY_SPECIES;
if (id === '' || id === 'constructor') return EMPTY_SPECIES;
let species: Mutable<Species> | undefined = this.speciesCache.get(id);
if (species) return species;

View File

@ -526,14 +526,8 @@ export class TeamValidator {
if (ruleTable.has('obtainableformes')) {
const canMegaEvo = dex.gen <= 7 || ruleTable.has('+pokemontag:past');
if (item.megaEvolves?.includes(species.name)) {
if (!item.megaStone) throw new Error(`Item ${item.name} has no base form for mega evolution`);
if (Array.isArray(item.megaEvolves)) {
const idx = item.megaEvolves.indexOf(species.name);
tierSpecies = dex.species.get(item.megaStone[idx]);
} else {
tierSpecies = dex.species.get(item.megaStone as string);
}
if (item.megaStone?.[species.name]) {
tierSpecies = dex.species.get(item.megaStone[species.name]);
} else if (item.id === 'redorb' && species.id === 'groudon') {
tierSpecies = dex.species.get('Groudon-Primal');
} else if (item.id === 'blueorb' && species.id === 'kyogre') {

View File

@ -137,8 +137,8 @@ export class ExhaustiveRunner {
const signatures = new Map();
for (const id of pools.items.possible) {
const item = dex.data.Items[id];
if (item.megaEvolves) {
const pokemon = toID(item.megaEvolves);
if (item.megaStone) {
const pokemon = toID(Object.keys(item.megaStone)[0]);
const combo = { item: id };
let combos = signatures.get(pokemon);
if (!combos) {

View File

@ -132,12 +132,6 @@ describe('Dex data', () => {
const entry = Items[itemid];
assert.equal(toID(entry.name), itemid, `Mismatched Item key "${itemid}" of "${entry.name}"`);
assert.equal(typeof entry.num, 'number', `Item ${entry.name} should have a number`);
if (entry.megaStone) {
assert.equal(typeof entry.megaStone, typeof entry.megaEvolves, `Item ${entry.name} megaStone and megaEvolves should both be the same type`);
if (Array.isArray(entry.megaStone)) {
assert.equal(entry.megaStone.length, entry.megaEvolves.length, `Item ${entry.name} megaStone and megaEvolves arrays should be the same length`);
}
}
}
});

View File

@ -312,7 +312,8 @@ function skip(dex: ModdedDex, format: Format, pokemon: string, set: DeepPartial<
if (pokemon === 'Rayquaza-Mega') {
return format.id.includes('ubers') || !hasMove('Dragon Ascent');
} else {
return dex.items.get(set.item).megaStone !== pokemon;
const item = dex.items.get(set.item);
return !item.megaStone || !Object.values(item.megaStone).includes(pokemon);
}
}
if (pokemon === 'Necrozma-Ultra' && set.item !== 'Ultranecrozium Z') return true;