Port evolution, Rare Candy, evolution stones and vitamins
Checks / web (push) Canceled after 0s

Pokémon that levelled up evolve after a won battle, following
EvolveAfterBattle's checks over the ROM's evolution bytes, with the
evolution screen that B can stop and move learning outside battle. The pack
uses Rare Candy, stones (with ABLE/NOT ABLE labels) and vitamins, and its
pocket tabs now sit in their own row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sébastien QUEROLandClaude Opus 5.5 committed 2026-09-30 16:21:59 +02:00
1 parent 1af2beacdc
commit 45275ba83b
19 files changed
+1032 -39

No files matched your search

+136 -7
View File
@@ -8,7 +8,8 @@ import {
restoredPP,
} from "../battle/battle";
import { receiveItem, removeFromSlot, type ItemSlot } from "../party/bag";
import { maxPP, type PartyMon } from "../party/pokemon";
import { calcStats, maxPP, type PartyMon } from "../party/pokemon";
import { PartyEvolution, evoStoneCompatible } from "./evolution";
import {
copyScriptState,
type MartEntry,
@@ -86,6 +87,10 @@ export interface StartMenuHost {
/** BikeFunction: the script that getting on or off the Bicycle queues
* here, or undefined when .CheckEnvironment refuses it. */
bicycleScript(state: ScriptState): number | undefined;
/** wTimeOfDay: morning 0, day 1, night 2. */
timeOfDay(): number;
/** GetWorldMapLocation for the current map (LevelUpHappinessMod). */
landmark(): number;
}
/** The overworld start menu (engine/menus/start_menu.asm) with its
@@ -269,7 +274,11 @@ export class StartMenuRun {
/** The party menu with PlacePartyMenuText's prompt. Returns the party
* index, or -1 when cancelled. `from` marks SwitchPartyMons' source. */
private *partyMenu(action: number, from?: number): Gen<number> {
private *partyMenu(
action: number,
from?: number,
item = 0,
): Gen<number> {
const r = this.reader,
entry = MENU.PartyMenuStrings + action * 2;
const prompt = this.string(r.near(entry, r.bank(entry)));
@@ -277,9 +286,17 @@ export class StartMenuRun {
const choice = yield* this.menu(
[prompt],
party.map((mon, slot) => (slot === from ? `▷${mon.nickname}` : mon.nickname)),
party.map(
(mon) =>
`:L${mon.level} HP ${mon.hp}/${mon.stats[0]}${mon.hp ? statusName(mon.status) : " FNT"}`,
party.map((mon) =>
// PlacePartyMonEvoStoneCompatibility replaces the HP bar.
action === C.PARTYMENUACTION_EVO_STONE
? mon.species === EGG
? ""
: this.string(
evoStoneCompatible(r, mon.species, item)
? MENU["PlacePartyMonEvoStoneCompatibility.string_able"]
: MENU["PlacePartyMonEvoStoneCompatibility.string_not_able"],
)
: `:L${mon.level} HP ${mon.hp}/${mon.stats[0]}${mon.hp ? statusName(mon.status) : " FNT"}`,
),
// InitPartyMenuWithCancel: the last row chosen, if still listed.
this.cursors.party >= 1 && this.cursors.party <= party.length
@@ -698,6 +715,12 @@ export class StartMenuRun {
}
case BATTLE.RestorePPEffect:
return yield* this.restorePPEffect(use);
case MENU.RareCandyEffect:
return yield* this.rareCandyEffect(use);
case MENU.VitaminEffect:
return yield* this.vitaminEffect(use);
case MENU.EvoStoneEffect:
return yield* this.evoStoneEffect(use);
case MENU.BicycleEffect: {
const script = this.host.bicycleScript(this.state);
if (script === undefined) return false;
@@ -712,8 +735,11 @@ export class StartMenuRun {
/** UseItem_SelectMon: the party menu for an item. The party index, or
* -1 when cancelled or an Egg was chosen. */
private *useItemSelectMon(): Gen<number> {
const slot = yield* this.partyMenu(PARTYMENUACTION_HEALING_ITEM);
private *useItemSelectMon(
action: number = PARTYMENUACTION_HEALING_ITEM,
item = 0,
): Gen<number> {
const slot = yield* this.partyMenu(action, undefined, item);
if (slot < 0) return -1;
if (this.state.party[slot].species === EGG) {
yield* this.print(BATTLE.ItemCantUseOnEggText);
@@ -825,6 +851,109 @@ export class StartMenuRun {
return 0;
}
/** Evolution and move learning on this menu's draft. */
private evolution() {
return new PartyEvolution(this.state, this.host);
}
/** RareCandyEffect: one level at the level's minimum experience, the
* stats (and HP, even of a fainted Pokémon) raised, the level-up
* happiness, new moves and a possible evolution. */
private *rareCandyEffect(use: FieldItemUse): Gen<boolean> {
const data = this.data;
const target = yield* this.useItemSelectMon();
if (target < 0) return false;
const mon = this.state.party[target];
this.strings[RAM.stringBuffer1] = mon.nickname;
if (mon.level >= C.MAX_LEVEL) {
yield* this.print(BATTLE.ItemWontHaveEffectText);
return false;
}
const level = ++mon.level;
mon.exp = data.expAtLevel(data.baseData(mon.species).growthRate, level);
const oldMax = mon.stats[0];
mon.stats = calcStats(data, mon.species, mon.dvs, level, mon.statExp);
mon.hp = (mon.hp + mon.stats[0] - oldMax) & 0xffff;
// LevelUpHappinessMod: more on the map where it was caught.
this.changeHappiness(
target,
(mon.caughtData & 0x7f) === this.host.landmark()
? C.HAPPINESS_GAINLEVELATHOME
: C.HAPPINESS_GAINLEVEL,
);
this.numbers[RAM.curPartyLevel] = level;
yield* this.itemActionText(target, C.PARTYMENUTEXT_LEVEL_UP);
// PrintTempMonStats: Attack, Defense, Sp. Atk, Sp. Def and Speed.
const labels = readString(
this.reader,
BATTLE["PrintTempMonStats.StatNames"],
this.text(),
64,
)
.split("\n")
.filter(Boolean);
const values = [1, 2, 4, 5, 3].map((stat) => mon.stats[stat]);
yield {
kind: "text",
pages: [labels.map((label, i) => `${label} ${values[i]}`).join("\n")],
};
const evolution = this.evolution();
yield* evolution.learnLevelMoves(target, level);
// EvolvePokemon, not forced: a level or happiness evolution.
yield* evolution.evolveAfterBattle(1 << target, false, use.item);
this.useDisposableItem(use);
return true;
}
/** VitaminEffect: ten more in the high byte of a stat's experience, up
* to 100, then CalcMonStats (the current HP stays). */
private *vitaminEffect(use: FieldItemUse): Gen<boolean> {
const r = this.reader;
const target = yield* this.useItemSelectMon();
if (target < 0) return false;
const mon = this.state.party[target];
this.strings[RAM.stringBuffer1] = mon.nickname;
// GetStatExpRelativePointer: the stat experience word for the item.
let p = MENU.StatExpItemPointerOffsets;
while (r.byte(p) !== use.item) p += 2;
const offset = r.byte(p + 1),
stat = offset >> 1;
if (mon.statExp[stat] >> 8 >= 100) {
yield* this.print(BATTLE.ItemWontHaveEffectText);
return false;
}
mon.statExp[stat] += 10 << 8;
mon.stats = calcStats(this.data, mon.species, mon.dvs, mon.level, mon.statExp);
const entry = MENU.StatStrings + offset;
this.strings[RAM.stringBuffer2] = this.string(r.near(entry, r.bank(entry)));
yield* this.print(MENU.ItemStatRoseText);
this.changeHappiness(target, C.HAPPINESS_USEDITEM);
this.useDisposableItem(use);
return true;
}
/** EvoStoneEffect: a forced EvolvePokemon with the stone as wCurItem.
* An Everstone or no matching evolution has no effect. */
private *evoStoneEffect(use: FieldItemUse): Gen<boolean> {
const target = yield* this.useItemSelectMon(
C.PARTYMENUACTION_EVO_STONE,
use.item,
);
if (target < 0) return false;
if (this.state.party[target].item === C.EVERSTONE) {
yield* this.print(BATTLE.ItemWontHaveEffectText);
return false;
}
if (
!(yield* this.evolution().evolveAfterBattle(1 << target, true, use.item))
) {
yield* this.print(BATTLE.ItemWontHaveEffectText);
return false;
}
this.useDisposableItem(use);
return true;
}
/** RestorePPEffect for Ethers, Elixers, the MysteryBerry and PP Up. B on
* the move list goes back to the party menu. */
private *restorePPEffect(use: FieldItemUse): Gen<boolean> {