Measure, weigh and name the European releases' own way (#84)
The French, German, Italian and Spanish releases store Magikarp's length in millimetres (and print a tenth of them), start the Magikarp house's record at their own length and holder, keep Pokédex heights in decimetres and weights in hectograms (printed in metres and kilograms), read the Heavy Ball's weight byte without converting it, and place the enemy's nickname before EnemyText (French, Italian) or the trainer's name before the class (French, Italian, Spanish). map-revision --symbols writes each release's symbol file, so the reference specs run against a release; specs that pin English data skip there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
627f87d8b5
commit
d3e3faff72
15 files changed
+246
-63
No files matched your search
+59
-3
@@ -1,11 +1,15 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { CrystalAssets } from "../src/game/rom/assets";
|
||||
import { JOHTO, MENU } from "../src/game/rom/profile";
|
||||
import { revisionAddresses, romRevision } from "../src/game/rom/revision";
|
||||
import { JOHTO, MENU, RAM } from "../src/game/rom/profile";
|
||||
import { initialMagikarpRecord, magikarpHouseSign } from "../src/game/scripts/lake-of-rage";
|
||||
import { magikarpLength, magikarpLengthBytes } from "../src/game/battle/battle";
|
||||
import { revisionAddresses, romRevision, useRevision } from "../src/game/rom/revision";
|
||||
import { RomReader } from "../src/game/rom/reader";
|
||||
import { RELEASES_IN_PROGRESS, validateRom } from "../src/rom/validate";
|
||||
import { readString } from "../src/game/scripts/text";
|
||||
import { defaultTextContext, readString } from "../src/game/scripts/text";
|
||||
import { textTiles } from "../src/game/rom/font";
|
||||
import { dexHeight, dexWeight } from "../src/game/rom/pokedex";
|
||||
import { Playthrough } from "./playthrough-driver";
|
||||
|
||||
/** The European releases at the repository root, each skipped when absent. */
|
||||
@@ -78,3 +82,55 @@ for (const { language, rom, release } of releases)
|
||||
expect(p.world.notice).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("the European releases' word order", () => {
|
||||
it("places the enemy's nickname before EnemyText in French and Italian", () => {
|
||||
// <USER> on the enemy's turn ($5a), then the terminator.
|
||||
const reader = new RomReader(new Uint8Array([0x5a, 0x50]));
|
||||
const name = (revision: Parameters<typeof useRevision>[0], prefix: string) => {
|
||||
useRevision(revision);
|
||||
return readString(reader, 0, {
|
||||
...defaultTextContext("KAWA"),
|
||||
battle: { turn: 1, player: "CYNDAQUIL", enemy: "RATTATA", enemyPrefix: prefix },
|
||||
});
|
||||
};
|
||||
expect(name("fr", " ennemi")).toBe("RATTATA ennemi");
|
||||
expect(name("it", " nemico")).toBe("RATTATA nemico");
|
||||
expect(name("de", "Gegn. ")).toBe("Gegn. RATTATA");
|
||||
expect(name("1.1", "Enemy ")).toBe("Enemy RATTATA");
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!existsSync(releases[0].rom))("the European releases' Magikarp", () => {
|
||||
it("measure in millimetres and print a tenth of them, from 105.3 by the release's own holder", () => {
|
||||
const bytes = new Uint8Array(readFileSync(releases[0].rom));
|
||||
const assets = new CrystalAssets(bytes);
|
||||
const record = initialMagikarpRecord(assets.reader);
|
||||
// InitializeMagikarpHouse's bytes: 1053 mm.
|
||||
expect([record.feet, record.inches]).toEqual([0x04, 0x1d]);
|
||||
expect(record.holder).not.toBe("RALPH");
|
||||
const strings: Record<number, string> = {};
|
||||
magikarpHouseSign({ strings, write: () => {} } as never, record);
|
||||
expect(strings[RAM.stringBuffer1]).toBe("105.3");
|
||||
// CalcMagikarpLength stores the millimetres (English converts them).
|
||||
const [high, low] = magikarpLengthBytes(assets.reader, 0x1234, 0x5678);
|
||||
expect((high << 8) | low).toBe(magikarpLength(assets.reader, 0x1234, 0x5678).mm);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!existsSync(releases[0].rom))("the European releases' Pokédex", () => {
|
||||
it("reads metric heights and weights and prints them in metres and kilograms", () => {
|
||||
for (const { rom } of releases) {
|
||||
if (!existsSync(rom)) continue;
|
||||
const dex = new CrystalAssets(new Uint8Array(readFileSync(rom))).pokedex;
|
||||
// PIKACHU: 0.4 m and 6.0 kg; its pages still follow.
|
||||
const entry = dex.entry(25);
|
||||
expect([entry.height, entry.weight]).toEqual([4, 60]);
|
||||
expect(entry.pages[0].length).toBeGreaterThan(10);
|
||||
const height = dexHeight(dex.dexString("Pokedex_DrawDexEntryScreenBG.Height"), entry.height);
|
||||
const weight = dexWeight(dex.dexString("Pokedex_DrawDexEntryScreenBG.Weight"), entry.weight);
|
||||
expect(height).toMatch(/^\S+ +0\.4m$/);
|
||||
expect(weight).toMatch(/^\S+ +6\.0kg$/);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -236,10 +236,13 @@ test.describe("Day-Care routines against the cartridge", () => {
|
||||
let text = "";
|
||||
for (const byte of bytes) {
|
||||
if (byte === 0x50) break;
|
||||
// Capitals and small letters (a release's own names use both).
|
||||
text +=
|
||||
byte >= 0x80 && byte < 0x9a
|
||||
? String.fromCharCode(65 + byte - 0x80)
|
||||
: "?";
|
||||
: byte >= 0xa0 && byte < 0xba
|
||||
? String.fromCharCode(97 + byte - 0xa0)
|
||||
: "?";
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
@@ -6,7 +6,10 @@ export const romPath = resolve(
|
||||
process.env.CRYSTAL_ROM_PATH ??
|
||||
"Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc",
|
||||
);
|
||||
export const symbolsPath = ".reference/pokecrystal/pokecrystal11.sym";
|
||||
/** PRET's 1.1 symbols, or CRYSTAL_SYMBOLS_PATH for another cartridge (a
|
||||
* release's symbols mapped from 1.1's, as scripts/map-revision.mjs finds
|
||||
* its labels). */
|
||||
export const symbolsPath = process.env.CRYSTAL_SYMBOLS_PATH ?? ".reference/pokecrystal/pokecrystal11.sym";
|
||||
/** PRET's rebuilds of the other English cartridges (`make crystal
|
||||
* crystal_au`), which keep 1.1's addresses for every label the port reads. */
|
||||
export const revisionRoms = {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Battle,
|
||||
applyTypeMatchups,
|
||||
calculateDamage,
|
||||
magikarpLength,
|
||||
magikarpLengthBytes,
|
||||
restoreHealth,
|
||||
restoredPP,
|
||||
sendOutText,
|
||||
@@ -708,8 +708,11 @@ test("native Magikarp lengths match CalcMagikarpLength", async ({ page }) => {
|
||||
cases.forEach((input, index) => {
|
||||
const { returned, ...original } = results[index];
|
||||
expect(returned, `case ${index} returned`).toBe(true);
|
||||
expect(magikarpLength(reader, input.dvs, input.id), JSON.stringify(input)).toEqual(original);
|
||||
// wMagikarpLength's bytes: feet and inches, or a European release's
|
||||
// millimetres.
|
||||
const [feet, inches] = magikarpLengthBytes(reader, input.dvs, input.id);
|
||||
expect({ feet, inches }, JSON.stringify(input)).toEqual(original);
|
||||
});
|
||||
const lengths = new Set(results.map(({ feet, inches }) => feet * 12 + inches));
|
||||
const lengths = new Set(results.map(({ feet, inches }) => feet * 256 + inches));
|
||||
expect(lengths.size).toBeGreaterThan(30);
|
||||
});
|
||||
@@ -3,6 +3,8 @@ import { existsSync } from "node:fs";
|
||||
import { romPath, symbolsPath, loadSymbols, openReference, revisionRoms } from "./reference-fixture";
|
||||
|
||||
test.skip(!existsSync(romPath) || !existsSync(symbolsPath), "Needs private ROM/reference.");
|
||||
// The English cartridges share 1.1's symbols; not run with another's.
|
||||
test.skip(Boolean(process.env.CRYSTAL_SYMBOLS_PATH), "Compares the English cartridges.");
|
||||
|
||||
/** BattleTowerText on each English cartridge, with PrintTextboxTextAt
|
||||
* patched to keep the text pointer: for every BattleTowerTrainers entry,
|
||||
|
||||
@@ -15,6 +15,10 @@ test.skip(!existsSync(romPath) || !existsSync(symbolsPath), "Needs private ROM a
|
||||
// and ObjectEventText's low byte ($f2), i.e. flag $f251. EventFlagAction's
|
||||
// bit math (de >> 3 added to wEventFlags) overflows by $1e4a bytes into
|
||||
// echo RAM, landing on wNumKeyItems (bank 1, $d8bc) bit 1.
|
||||
// These pin English 1.1's data (ObjectEvent's bytes read as a header, its
|
||||
// trainers and texts); another release's data differs.
|
||||
test.skip(Boolean(process.env.CRYSTAL_SYMBOLS_PATH), "Checks English data.");
|
||||
|
||||
test("CheckTrainerBattle reads BurnedTower1F's rival as beaten only when wNumKeyItems' bit 1 is set", async ({ page }) => {
|
||||
const { at, routine } = loadSymbols();
|
||||
const a = {
|
||||
|
||||
Reference in new issue
Block a user