Merge pull request #2491 from bryanthaboi/dev

This commit is contained in:
bryanthaboi
2026-09-25 20:32:33 -04:00
committed by GitHub
90 changed files with 2441 additions and 543 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 34 KiB

@@ -9,4 +9,10 @@
<color name="shortcut_yellow">#FDD835</color>
<color name="shortcut_gold">#D4AF37</color>
<color name="shortcut_silver">#BEC6D2</color>
<color name="shortcut_crystal">#6EC3DF</color>
<color name="shortcut_firered">#EB4123</color>
<color name="shortcut_leafgreen">#28AF50</color>
<color name="shortcut_ruby">#C31E32</color>
<color name="shortcut_sapphire">#1946B9</color>
<color name="shortcut_emerald">#149B50</color>
</resources>
@@ -937,6 +937,41 @@ public class GameActivity extends SDLActivity {
longLabel = "Play Gold";
iconResId = context.getResources().getIdentifier("ic_shortcut_gold", "drawable", context.getPackageName());
break;
case "silver":
shortLabel = "Play Silver";
longLabel = "Play Silver";
iconResId = context.getResources().getIdentifier("ic_shortcut_silver", "drawable", context.getPackageName());
break;
case "crystal":
shortLabel = "Play Crystal";
longLabel = "Play Crystal";
iconResId = context.getResources().getIdentifier("ic_shortcut_crystal", "drawable", context.getPackageName());
break;
case "firered":
shortLabel = "Play FireRed";
longLabel = "Play FireRed";
iconResId = context.getResources().getIdentifier("ic_shortcut_firered", "drawable", context.getPackageName());
break;
case "leafgreen":
shortLabel = "Play LeafGreen";
longLabel = "Play LeafGreen";
iconResId = context.getResources().getIdentifier("ic_shortcut_leafgreen", "drawable", context.getPackageName());
break;
case "ruby":
shortLabel = "Play Ruby";
longLabel = "Play Ruby";
iconResId = context.getResources().getIdentifier("ic_shortcut_ruby", "drawable", context.getPackageName());
break;
case "sapphire":
shortLabel = "Play Sapphire";
longLabel = "Play Sapphire";
iconResId = context.getResources().getIdentifier("ic_shortcut_sapphire", "drawable", context.getPackageName());
break;
case "emerald":
shortLabel = "Play Emerald";
longLabel = "Play Emerald";
iconResId = context.getResources().getIdentifier("ic_shortcut_emerald", "drawable", context.getPackageName());
break;
default:
String capitalized = lower.substring(0, 1).toUpperCase() + lower.substring(1);
shortLabel = "Play " + capitalized;
@@ -2157,35 +2192,9 @@ public class GameActivity extends SDLActivity {
}
private void requestGameAudioFocus() {
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
if (audioManager == null) {
return;
}
if (audioFocusListener == null) {
audioFocusListener = new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
handleAudioFocusChange(focusChange);
}
};
}
int result;
if (android.os.Build.VERSION.SDK_INT >= 26) {
result = requestGameAudioFocusModern(audioManager);
} else {
result = audioManager.requestAudioFocus(audioFocusListener,
AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
}
audioFocusHeld = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
if (!audioFocusHeld) {
Log.d("GameActivity", "audio focus request was not granted: " + result);
}
// Do not request exclusive AUDIOFOCUS_GAIN to allow background media
// (Spotify, YouTube, podcasts, etc.) to continue playing seamlessly.
// Android's native audio mixer will mix game audio with background apps.
}
private int requestGameAudioFocusModern(AudioManager audioManager) {
@@ -2233,14 +2242,9 @@ public class GameActivity extends SDLActivity {
try {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_LOSS:
audioFocusHeld = false;
nativeAudioFocusLost();
scheduleAudioFocusRecovery();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
nativeAudioFocusLost();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// Allow mixing with other audio streams without pausing game audio
break;
case AudioManager.AUDIOFOCUS_GAIN:
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
+361
View File
@@ -0,0 +1,361 @@
-- Canonical Gen 3 (FireRed) Move Table builtin fallback.
-- Generated from pokefirered battle_moves.h and move_names.h.
local BuiltinMoves = {
[0] = { id = 0, name = "-$$$$$$", pp = 0, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "HIT" },
[1] = { id = 1, name = "POUND", pp = 35, power = 40, accuracy = 100, type = "NORMAL", priority = 0, effect = "HIT" },
[2] = { id = 2, name = "KARATE CHOP", pp = 25, power = 50, accuracy = 100, type = "FIGHTING", priority = 0, effect = "HIGH_CRITICAL" },
[3] = { id = 3, name = "DOUBLESLAP", pp = 10, power = 15, accuracy = 85, type = "NORMAL", priority = 0, effect = "MULTI_HIT" },
[4] = { id = 4, name = "COMET PUNCH", pp = 15, power = 18, accuracy = 85, type = "NORMAL", priority = 0, effect = "MULTI_HIT" },
[5] = { id = 5, name = "MEGA PUNCH", pp = 20, power = 80, accuracy = 85, type = "NORMAL", priority = 0, effect = "HIT" },
[6] = { id = 6, name = "PAY DAY", pp = 20, power = 40, accuracy = 100, type = "NORMAL", priority = 0, effect = "PAY_DAY" },
[7] = { id = 7, name = "FIRE PUNCH", pp = 15, power = 75, accuracy = 100, type = "FIRE", priority = 0, effect = "BURN_HIT" },
[8] = { id = 8, name = "ICE PUNCH", pp = 15, power = 75, accuracy = 100, type = "ICE", priority = 0, effect = "FREEZE_HIT" },
[9] = { id = 9, name = "THUNDERPUNCH", pp = 15, power = 75, accuracy = 100, type = "ELECTRIC", priority = 0, effect = "PARALYZE_HIT" },
[10] = { id = 10, name = "SCRATCH", pp = 35, power = 40, accuracy = 100, type = "NORMAL", priority = 0, effect = "HIT" },
[11] = { id = 11, name = "VICEGRIP", pp = 30, power = 55, accuracy = 100, type = "NORMAL", priority = 0, effect = "HIT" },
[12] = { id = 12, name = "GUILLOTINE", pp = 5, power = 1, accuracy = 30, type = "NORMAL", priority = 0, effect = "OHKO" },
[13] = { id = 13, name = "RAZOR WIND", pp = 10, power = 80, accuracy = 100, type = "NORMAL", priority = 0, effect = "RAZOR_WIND" },
[14] = { id = 14, name = "SWORDS DANCE", pp = 30, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "ATTACK_UP_2" },
[15] = { id = 15, name = "CUT", pp = 30, power = 50, accuracy = 95, type = "NORMAL", priority = 0, effect = "HIT" },
[16] = { id = 16, name = "GUST", pp = 35, power = 40, accuracy = 100, type = "FLYING", priority = 0, effect = "GUST" },
[17] = { id = 17, name = "WING ATTACK", pp = 35, power = 60, accuracy = 100, type = "FLYING", priority = 0, effect = "HIT" },
[18] = { id = 18, name = "WHIRLWIND", pp = 20, power = 0, accuracy = 100, type = "NORMAL", priority = -6, effect = "ROAR" },
[19] = { id = 19, name = "FLY", pp = 15, power = 70, accuracy = 95, type = "FLYING", priority = 0, effect = "SEMI_INVULNERABLE" },
[20] = { id = 20, name = "BIND", pp = 20, power = 15, accuracy = 75, type = "NORMAL", priority = 0, effect = "TRAP" },
[21] = { id = 21, name = "SLAM", pp = 20, power = 80, accuracy = 75, type = "NORMAL", priority = 0, effect = "HIT" },
[22] = { id = 22, name = "VINE WHIP", pp = 10, power = 35, accuracy = 100, type = "GRASS", priority = 0, effect = "HIT" },
[23] = { id = 23, name = "STOMP", pp = 20, power = 65, accuracy = 100, type = "NORMAL", priority = 0, effect = "FLINCH_MINIMIZE_HIT" },
[24] = { id = 24, name = "DOUBLE KICK", pp = 30, power = 30, accuracy = 100, type = "FIGHTING", priority = 0, effect = "DOUBLE_HIT" },
[25] = { id = 25, name = "MEGA KICK", pp = 5, power = 120, accuracy = 75, type = "NORMAL", priority = 0, effect = "HIT" },
[26] = { id = 26, name = "JUMP KICK", pp = 25, power = 70, accuracy = 95, type = "FIGHTING", priority = 0, effect = "RECOIL_IF_MISS" },
[27] = { id = 27, name = "ROLLING KICK", pp = 15, power = 60, accuracy = 85, type = "FIGHTING", priority = 0, effect = "FLINCH_HIT" },
[28] = { id = 28, name = "SAND-ATTACK", pp = 15, power = 0, accuracy = 100, type = "GROUND", priority = 0, effect = "ACCURACY_DOWN" },
[29] = { id = 29, name = "HEADBUTT", pp = 15, power = 70, accuracy = 100, type = "NORMAL", priority = 0, effect = "FLINCH_HIT" },
[30] = { id = 30, name = "HORN ATTACK", pp = 25, power = 65, accuracy = 100, type = "NORMAL", priority = 0, effect = "HIT" },
[31] = { id = 31, name = "FURY ATTACK", pp = 20, power = 15, accuracy = 85, type = "NORMAL", priority = 0, effect = "MULTI_HIT" },
[32] = { id = 32, name = "HORN DRILL", pp = 5, power = 1, accuracy = 30, type = "NORMAL", priority = 0, effect = "OHKO" },
[33] = { id = 33, name = "TACKLE", pp = 35, power = 35, accuracy = 95, type = "NORMAL", priority = 0, effect = "HIT" },
[34] = { id = 34, name = "BODY SLAM", pp = 15, power = 85, accuracy = 100, type = "NORMAL", priority = 0, effect = "PARALYZE_HIT" },
[35] = { id = 35, name = "WRAP", pp = 20, power = 15, accuracy = 85, type = "NORMAL", priority = 0, effect = "TRAP" },
[36] = { id = 36, name = "TAKE DOWN", pp = 20, power = 90, accuracy = 85, type = "NORMAL", priority = 0, effect = "RECOIL" },
[37] = { id = 37, name = "THRASH", pp = 20, power = 90, accuracy = 100, type = "NORMAL", priority = 0, effect = "RAMPAGE" },
[38] = { id = 38, name = "DOUBLE-EDGE", pp = 15, power = 120, accuracy = 100, type = "NORMAL", priority = 0, effect = "DOUBLE_EDGE" },
[39] = { id = 39, name = "TAIL WHIP", pp = 30, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "DEFENSE_DOWN" },
[40] = { id = 40, name = "POISON STING", pp = 35, power = 15, accuracy = 100, type = "POISON", priority = 0, effect = "POISON_HIT" },
[41] = { id = 41, name = "TWINEEDLE", pp = 20, power = 25, accuracy = 100, type = "BUG", priority = 0, effect = "TWINEEDLE" },
[42] = { id = 42, name = "PIN MISSILE", pp = 20, power = 14, accuracy = 85, type = "BUG", priority = 0, effect = "MULTI_HIT" },
[43] = { id = 43, name = "LEER", pp = 30, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "DEFENSE_DOWN" },
[44] = { id = 44, name = "BITE", pp = 25, power = 60, accuracy = 100, type = "DARK", priority = 0, effect = "FLINCH_HIT" },
[45] = { id = 45, name = "GROWL", pp = 40, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "ATTACK_DOWN" },
[46] = { id = 46, name = "ROAR", pp = 20, power = 0, accuracy = 100, type = "NORMAL", priority = -6, effect = "ROAR" },
[47] = { id = 47, name = "SING", pp = 15, power = 0, accuracy = 55, type = "NORMAL", priority = 0, effect = "SLEEP" },
[48] = { id = 48, name = "SUPERSONIC", pp = 20, power = 0, accuracy = 55, type = "NORMAL", priority = 0, effect = "CONFUSE" },
[49] = { id = 49, name = "SONICBOOM", pp = 20, power = 1, accuracy = 90, type = "NORMAL", priority = 0, effect = "SONICBOOM" },
[50] = { id = 50, name = "DISABLE", pp = 20, power = 0, accuracy = 55, type = "NORMAL", priority = 0, effect = "DISABLE" },
[51] = { id = 51, name = "ACID", pp = 30, power = 40, accuracy = 100, type = "POISON", priority = 0, effect = "DEFENSE_DOWN_HIT" },
[52] = { id = 52, name = "EMBER", pp = 25, power = 40, accuracy = 100, type = "FIRE", priority = 0, effect = "BURN_HIT" },
[53] = { id = 53, name = "FLAMETHROWER", pp = 15, power = 95, accuracy = 100, type = "FIRE", priority = 0, effect = "BURN_HIT" },
[54] = { id = 54, name = "MIST", pp = 30, power = 0, accuracy = 0, type = "ICE", priority = 0, effect = "MIST" },
[55] = { id = 55, name = "WATER GUN", pp = 25, power = 40, accuracy = 100, type = "WATER", priority = 0, effect = "HIT" },
[56] = { id = 56, name = "HYDRO PUMP", pp = 5, power = 120, accuracy = 80, type = "WATER", priority = 0, effect = "HIT" },
[57] = { id = 57, name = "SURF", pp = 15, power = 95, accuracy = 100, type = "WATER", priority = 0, effect = "HIT" },
[58] = { id = 58, name = "ICE BEAM", pp = 10, power = 95, accuracy = 100, type = "ICE", priority = 0, effect = "FREEZE_HIT" },
[59] = { id = 59, name = "BLIZZARD", pp = 5, power = 120, accuracy = 70, type = "ICE", priority = 0, effect = "FREEZE_HIT" },
[60] = { id = 60, name = "PSYBEAM", pp = 20, power = 65, accuracy = 100, type = "PSYCHIC", priority = 0, effect = "CONFUSE_HIT" },
[61] = { id = 61, name = "BUBBLEBEAM", pp = 20, power = 65, accuracy = 100, type = "WATER", priority = 0, effect = "SPEED_DOWN_HIT" },
[62] = { id = 62, name = "AURORA BEAM", pp = 20, power = 65, accuracy = 100, type = "ICE", priority = 0, effect = "ATTACK_DOWN_HIT" },
[63] = { id = 63, name = "HYPER BEAM", pp = 5, power = 150, accuracy = 90, type = "NORMAL", priority = 0, effect = "RECHARGE" },
[64] = { id = 64, name = "PECK", pp = 35, power = 35, accuracy = 100, type = "FLYING", priority = 0, effect = "HIT" },
[65] = { id = 65, name = "DRILL PECK", pp = 20, power = 80, accuracy = 100, type = "FLYING", priority = 0, effect = "HIT" },
[66] = { id = 66, name = "SUBMISSION", pp = 25, power = 80, accuracy = 80, type = "FIGHTING", priority = 0, effect = "RECOIL" },
[67] = { id = 67, name = "LOW KICK", pp = 20, power = 1, accuracy = 100, type = "FIGHTING", priority = 0, effect = "LOW_KICK" },
[68] = { id = 68, name = "COUNTER", pp = 20, power = 1, accuracy = 100, type = "FIGHTING", priority = -5, effect = "COUNTER" },
[69] = { id = 69, name = "SEISMIC TOSS", pp = 20, power = 1, accuracy = 100, type = "FIGHTING", priority = 0, effect = "LEVEL_DAMAGE" },
[70] = { id = 70, name = "STRENGTH", pp = 15, power = 80, accuracy = 100, type = "NORMAL", priority = 0, effect = "HIT" },
[71] = { id = 71, name = "ABSORB", pp = 20, power = 20, accuracy = 100, type = "GRASS", priority = 0, effect = "ABSORB" },
[72] = { id = 72, name = "MEGA DRAIN", pp = 10, power = 40, accuracy = 100, type = "GRASS", priority = 0, effect = "ABSORB" },
[73] = { id = 73, name = "LEECH SEED", pp = 10, power = 0, accuracy = 90, type = "GRASS", priority = 0, effect = "LEECH_SEED" },
[74] = { id = 74, name = "GROWTH", pp = 40, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "SPECIAL_ATTACK_UP" },
[75] = { id = 75, name = "RAZOR LEAF", pp = 25, power = 55, accuracy = 95, type = "GRASS", priority = 0, effect = "HIGH_CRITICAL" },
[76] = { id = 76, name = "SOLARBEAM", pp = 10, power = 120, accuracy = 100, type = "GRASS", priority = 0, effect = "SOLAR_BEAM" },
[77] = { id = 77, name = "POISONPOWDER", pp = 35, power = 0, accuracy = 75, type = "POISON", priority = 0, effect = "POISON" },
[78] = { id = 78, name = "STUN SPORE", pp = 30, power = 0, accuracy = 75, type = "GRASS", priority = 0, effect = "PARALYZE" },
[79] = { id = 79, name = "SLEEP POWDER", pp = 15, power = 0, accuracy = 75, type = "GRASS", priority = 0, effect = "SLEEP" },
[80] = { id = 80, name = "PETAL DANCE", pp = 20, power = 70, accuracy = 100, type = "GRASS", priority = 0, effect = "RAMPAGE" },
[81] = { id = 81, name = "STRING SHOT", pp = 40, power = 0, accuracy = 95, type = "BUG", priority = 0, effect = "SPEED_DOWN" },
[82] = { id = 82, name = "DRAGON RAGE", pp = 10, power = 1, accuracy = 100, type = "DRAGON", priority = 0, effect = "DRAGON_RAGE" },
[83] = { id = 83, name = "FIRE SPIN", pp = 15, power = 15, accuracy = 70, type = "FIRE", priority = 0, effect = "TRAP" },
[84] = { id = 84, name = "THUNDERSHOCK", pp = 30, power = 40, accuracy = 100, type = "ELECTRIC", priority = 0, effect = "PARALYZE_HIT" },
[85] = { id = 85, name = "THUNDERBOLT", pp = 15, power = 95, accuracy = 100, type = "ELECTRIC", priority = 0, effect = "PARALYZE_HIT" },
[86] = { id = 86, name = "THUNDER WAVE", pp = 20, power = 0, accuracy = 100, type = "ELECTRIC", priority = 0, effect = "PARALYZE" },
[87] = { id = 87, name = "THUNDER", pp = 10, power = 120, accuracy = 70, type = "ELECTRIC", priority = 0, effect = "THUNDER" },
[88] = { id = 88, name = "ROCK THROW", pp = 15, power = 50, accuracy = 90, type = "ROCK", priority = 0, effect = "HIT" },
[89] = { id = 89, name = "EARTHQUAKE", pp = 10, power = 100, accuracy = 100, type = "GROUND", priority = 0, effect = "EARTHQUAKE" },
[90] = { id = 90, name = "FISSURE", pp = 5, power = 1, accuracy = 30, type = "GROUND", priority = 0, effect = "OHKO" },
[91] = { id = 91, name = "DIG", pp = 10, power = 60, accuracy = 100, type = "GROUND", priority = 0, effect = "SEMI_INVULNERABLE" },
[92] = { id = 92, name = "TOXIC", pp = 10, power = 0, accuracy = 85, type = "POISON", priority = 0, effect = "TOXIC" },
[93] = { id = 93, name = "CONFUSION", pp = 25, power = 50, accuracy = 100, type = "PSYCHIC", priority = 0, effect = "CONFUSE_HIT" },
[94] = { id = 94, name = "PSYCHIC", pp = 10, power = 90, accuracy = 100, type = "PSYCHIC", priority = 0, effect = "SPECIAL_DEFENSE_DOWN_HIT" },
[95] = { id = 95, name = "HYPNOSIS", pp = 20, power = 0, accuracy = 60, type = "PSYCHIC", priority = 0, effect = "SLEEP" },
[96] = { id = 96, name = "MEDITATE", pp = 40, power = 0, accuracy = 0, type = "PSYCHIC", priority = 0, effect = "ATTACK_UP" },
[97] = { id = 97, name = "AGILITY", pp = 30, power = 0, accuracy = 0, type = "PSYCHIC", priority = 0, effect = "SPEED_UP_2" },
[98] = { id = 98, name = "QUICK ATTACK", pp = 30, power = 40, accuracy = 100, type = "NORMAL", priority = 1, effect = "QUICK_ATTACK" },
[99] = { id = 99, name = "RAGE", pp = 20, power = 20, accuracy = 100, type = "NORMAL", priority = 0, effect = "RAGE" },
[100] = { id = 100, name = "TELEPORT", pp = 20, power = 0, accuracy = 0, type = "PSYCHIC", priority = 0, effect = "TELEPORT" },
[101] = { id = 101, name = "NIGHT SHADE", pp = 15, power = 1, accuracy = 100, type = "GHOST", priority = 0, effect = "LEVEL_DAMAGE" },
[102] = { id = 102, name = "MIMIC", pp = 10, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "MIMIC" },
[103] = { id = 103, name = "SCREECH", pp = 40, power = 0, accuracy = 85, type = "NORMAL", priority = 0, effect = "DEFENSE_DOWN_2" },
[104] = { id = 104, name = "DOUBLE TEAM", pp = 15, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "EVASION_UP" },
[105] = { id = 105, name = "RECOVER", pp = 20, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "RESTORE_HP" },
[106] = { id = 106, name = "HARDEN", pp = 30, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "DEFENSE_UP" },
[107] = { id = 107, name = "MINIMIZE", pp = 20, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "MINIMIZE" },
[108] = { id = 108, name = "SMOKESCREEN", pp = 20, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "ACCURACY_DOWN" },
[109] = { id = 109, name = "CONFUSE RAY", pp = 10, power = 0, accuracy = 100, type = "GHOST", priority = 0, effect = "CONFUSE" },
[110] = { id = 110, name = "WITHDRAW", pp = 40, power = 0, accuracy = 0, type = "WATER", priority = 0, effect = "DEFENSE_UP" },
[111] = { id = 111, name = "DEFENSE CURL", pp = 40, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "DEFENSE_CURL" },
[112] = { id = 112, name = "BARRIER", pp = 30, power = 0, accuracy = 0, type = "PSYCHIC", priority = 0, effect = "DEFENSE_UP_2" },
[113] = { id = 113, name = "LIGHT SCREEN", pp = 30, power = 0, accuracy = 0, type = "PSYCHIC", priority = 0, effect = "LIGHT_SCREEN" },
[114] = { id = 114, name = "HAZE", pp = 30, power = 0, accuracy = 0, type = "ICE", priority = 0, effect = "HAZE" },
[115] = { id = 115, name = "REFLECT", pp = 20, power = 0, accuracy = 0, type = "PSYCHIC", priority = 0, effect = "REFLECT" },
[116] = { id = 116, name = "FOCUS ENERGY", pp = 30, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "FOCUS_ENERGY" },
[117] = { id = 117, name = "BIDE", pp = 10, power = 1, accuracy = 100, type = "NORMAL", priority = 0, effect = "BIDE" },
[118] = { id = 118, name = "METRONOME", pp = 10, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "METRONOME" },
[119] = { id = 119, name = "MIRROR MOVE", pp = 20, power = 0, accuracy = 0, type = "FLYING", priority = 0, effect = "MIRROR_MOVE" },
[120] = { id = 120, name = "SELFDESTRUCT", pp = 5, power = 200, accuracy = 100, type = "NORMAL", priority = 0, effect = "EXPLOSION" },
[121] = { id = 121, name = "EGG BOMB", pp = 10, power = 100, accuracy = 75, type = "NORMAL", priority = 0, effect = "HIT" },
[122] = { id = 122, name = "LICK", pp = 30, power = 20, accuracy = 100, type = "GHOST", priority = 0, effect = "PARALYZE_HIT" },
[123] = { id = 123, name = "SMOG", pp = 20, power = 20, accuracy = 70, type = "POISON", priority = 0, effect = "POISON_HIT" },
[124] = { id = 124, name = "SLUDGE", pp = 20, power = 65, accuracy = 100, type = "POISON", priority = 0, effect = "POISON_HIT" },
[125] = { id = 125, name = "BONE CLUB", pp = 20, power = 65, accuracy = 85, type = "GROUND", priority = 0, effect = "FLINCH_HIT" },
[126] = { id = 126, name = "FIRE BLAST", pp = 5, power = 120, accuracy = 85, type = "FIRE", priority = 0, effect = "BURN_HIT" },
[127] = { id = 127, name = "WATERFALL", pp = 15, power = 80, accuracy = 100, type = "WATER", priority = 0, effect = "HIT" },
[128] = { id = 128, name = "CLAMP", pp = 10, power = 35, accuracy = 75, type = "WATER", priority = 0, effect = "TRAP" },
[129] = { id = 129, name = "SWIFT", pp = 20, power = 60, accuracy = 0, type = "NORMAL", priority = 0, effect = "ALWAYS_HIT" },
[130] = { id = 130, name = "SKULL BASH", pp = 15, power = 100, accuracy = 100, type = "NORMAL", priority = 0, effect = "SKULL_BASH" },
[131] = { id = 131, name = "SPIKE CANNON", pp = 15, power = 20, accuracy = 100, type = "NORMAL", priority = 0, effect = "MULTI_HIT" },
[132] = { id = 132, name = "CONSTRICT", pp = 35, power = 10, accuracy = 100, type = "NORMAL", priority = 0, effect = "SPEED_DOWN_HIT" },
[133] = { id = 133, name = "AMNESIA", pp = 20, power = 0, accuracy = 0, type = "PSYCHIC", priority = 0, effect = "SPECIAL_DEFENSE_UP_2" },
[134] = { id = 134, name = "KINESIS", pp = 15, power = 0, accuracy = 80, type = "PSYCHIC", priority = 0, effect = "ACCURACY_DOWN" },
[135] = { id = 135, name = "SOFTBOILED", pp = 10, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "SOFTBOILED" },
[136] = { id = 136, name = "HI JUMP KICK", pp = 20, power = 85, accuracy = 90, type = "FIGHTING", priority = 0, effect = "RECOIL_IF_MISS" },
[137] = { id = 137, name = "GLARE", pp = 30, power = 0, accuracy = 75, type = "NORMAL", priority = 0, effect = "PARALYZE" },
[138] = { id = 138, name = "DREAM EATER", pp = 15, power = 100, accuracy = 100, type = "PSYCHIC", priority = 0, effect = "DREAM_EATER" },
[139] = { id = 139, name = "POISON GAS", pp = 40, power = 0, accuracy = 55, type = "POISON", priority = 0, effect = "POISON" },
[140] = { id = 140, name = "BARRAGE", pp = 20, power = 15, accuracy = 85, type = "NORMAL", priority = 0, effect = "MULTI_HIT" },
[141] = { id = 141, name = "LEECH LIFE", pp = 15, power = 20, accuracy = 100, type = "BUG", priority = 0, effect = "ABSORB" },
[142] = { id = 142, name = "LOVELY KISS", pp = 10, power = 0, accuracy = 75, type = "NORMAL", priority = 0, effect = "SLEEP" },
[143] = { id = 143, name = "SKY ATTACK", pp = 5, power = 140, accuracy = 90, type = "FLYING", priority = 0, effect = "SKY_ATTACK" },
[144] = { id = 144, name = "TRANSFORM", pp = 10, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "TRANSFORM" },
[145] = { id = 145, name = "BUBBLE", pp = 30, power = 20, accuracy = 100, type = "WATER", priority = 0, effect = "SPEED_DOWN_HIT" },
[146] = { id = 146, name = "DIZZY PUNCH", pp = 10, power = 70, accuracy = 100, type = "NORMAL", priority = 0, effect = "CONFUSE_HIT" },
[147] = { id = 147, name = "SPORE", pp = 15, power = 0, accuracy = 100, type = "GRASS", priority = 0, effect = "SLEEP" },
[148] = { id = 148, name = "FLASH", pp = 20, power = 0, accuracy = 70, type = "NORMAL", priority = 0, effect = "ACCURACY_DOWN" },
[149] = { id = 149, name = "PSYWAVE", pp = 15, power = 1, accuracy = 80, type = "PSYCHIC", priority = 0, effect = "PSYWAVE" },
[150] = { id = 150, name = "SPLASH", pp = 40, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "SPLASH" },
[151] = { id = 151, name = "ACID ARMOR", pp = 40, power = 0, accuracy = 0, type = "POISON", priority = 0, effect = "DEFENSE_UP_2" },
[152] = { id = 152, name = "CRABHAMMER", pp = 10, power = 90, accuracy = 85, type = "WATER", priority = 0, effect = "HIGH_CRITICAL" },
[153] = { id = 153, name = "EXPLOSION", pp = 5, power = 250, accuracy = 100, type = "NORMAL", priority = 0, effect = "EXPLOSION" },
[154] = { id = 154, name = "FURY SWIPES", pp = 15, power = 18, accuracy = 80, type = "NORMAL", priority = 0, effect = "MULTI_HIT" },
[155] = { id = 155, name = "BONEMERANG", pp = 10, power = 50, accuracy = 90, type = "GROUND", priority = 0, effect = "DOUBLE_HIT" },
[156] = { id = 156, name = "REST", pp = 10, power = 0, accuracy = 0, type = "PSYCHIC", priority = 0, effect = "REST" },
[157] = { id = 157, name = "ROCK SLIDE", pp = 10, power = 75, accuracy = 90, type = "ROCK", priority = 0, effect = "FLINCH_HIT" },
[158] = { id = 158, name = "HYPER FANG", pp = 15, power = 80, accuracy = 90, type = "NORMAL", priority = 0, effect = "FLINCH_HIT" },
[159] = { id = 159, name = "SHARPEN", pp = 30, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "ATTACK_UP" },
[160] = { id = 160, name = "CONVERSION", pp = 30, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "CONVERSION" },
[161] = { id = 161, name = "TRI ATTACK", pp = 10, power = 80, accuracy = 100, type = "NORMAL", priority = 0, effect = "TRI_ATTACK" },
[162] = { id = 162, name = "SUPER FANG", pp = 10, power = 1, accuracy = 90, type = "NORMAL", priority = 0, effect = "SUPER_FANG" },
[163] = { id = 163, name = "SLASH", pp = 20, power = 70, accuracy = 100, type = "NORMAL", priority = 0, effect = "HIGH_CRITICAL" },
[164] = { id = 164, name = "SUBSTITUTE", pp = 10, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "SUBSTITUTE" },
[165] = { id = 165, name = "STRUGGLE", pp = 1, power = 50, accuracy = 100, type = "NORMAL", priority = 0, effect = "RECOIL" },
[166] = { id = 166, name = "SKETCH", pp = 1, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "SKETCH" },
[167] = { id = 167, name = "TRIPLE KICK", pp = 10, power = 10, accuracy = 90, type = "FIGHTING", priority = 0, effect = "TRIPLE_KICK" },
[168] = { id = 168, name = "THIEF", pp = 10, power = 40, accuracy = 100, type = "DARK", priority = 0, effect = "THIEF" },
[169] = { id = 169, name = "SPIDER WEB", pp = 10, power = 0, accuracy = 100, type = "BUG", priority = 0, effect = "MEAN_LOOK" },
[170] = { id = 170, name = "MIND READER", pp = 5, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "LOCK_ON" },
[171] = { id = 171, name = "NIGHTMARE", pp = 15, power = 0, accuracy = 100, type = "GHOST", priority = 0, effect = "NIGHTMARE" },
[172] = { id = 172, name = "FLAME WHEEL", pp = 25, power = 60, accuracy = 100, type = "FIRE", priority = 0, effect = "THAW_HIT" },
[173] = { id = 173, name = "SNORE", pp = 15, power = 40, accuracy = 100, type = "NORMAL", priority = 0, effect = "SNORE" },
[174] = { id = 174, name = "CURSE", pp = 10, power = 0, accuracy = 0, type = "MYSTERY", priority = 0, effect = "CURSE" },
[175] = { id = 175, name = "FLAIL", pp = 15, power = 1, accuracy = 100, type = "NORMAL", priority = 0, effect = "FLAIL" },
[176] = { id = 176, name = "CONVERSION 2", pp = 30, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "CONVERSION_2" },
[177] = { id = 177, name = "AEROBLAST", pp = 5, power = 100, accuracy = 95, type = "FLYING", priority = 0, effect = "HIGH_CRITICAL" },
[178] = { id = 178, name = "COTTON SPORE", pp = 40, power = 0, accuracy = 85, type = "GRASS", priority = 0, effect = "SPEED_DOWN_2" },
[179] = { id = 179, name = "REVERSAL", pp = 15, power = 1, accuracy = 100, type = "FIGHTING", priority = 0, effect = "FLAIL" },
[180] = { id = 180, name = "SPITE", pp = 10, power = 0, accuracy = 100, type = "GHOST", priority = 0, effect = "SPITE" },
[181] = { id = 181, name = "POWDER SNOW", pp = 25, power = 40, accuracy = 100, type = "ICE", priority = 0, effect = "FREEZE_HIT" },
[182] = { id = 182, name = "PROTECT", pp = 10, power = 0, accuracy = 0, type = "NORMAL", priority = 3, effect = "PROTECT" },
[183] = { id = 183, name = "MACH PUNCH", pp = 30, power = 40, accuracy = 100, type = "FIGHTING", priority = 1, effect = "QUICK_ATTACK" },
[184] = { id = 184, name = "SCARY FACE", pp = 10, power = 0, accuracy = 90, type = "NORMAL", priority = 0, effect = "SPEED_DOWN_2" },
[185] = { id = 185, name = "FAINT ATTACK", pp = 20, power = 60, accuracy = 0, type = "DARK", priority = 0, effect = "ALWAYS_HIT" },
[186] = { id = 186, name = "SWEET KISS", pp = 10, power = 0, accuracy = 75, type = "NORMAL", priority = 0, effect = "CONFUSE" },
[187] = { id = 187, name = "BELLY DRUM", pp = 10, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "BELLY_DRUM" },
[188] = { id = 188, name = "SLUDGE BOMB", pp = 10, power = 90, accuracy = 100, type = "POISON", priority = 0, effect = "POISON_HIT" },
[189] = { id = 189, name = "MUD-SLAP", pp = 10, power = 20, accuracy = 100, type = "GROUND", priority = 0, effect = "ACCURACY_DOWN_HIT" },
[190] = { id = 190, name = "OCTAZOOKA", pp = 10, power = 65, accuracy = 85, type = "WATER", priority = 0, effect = "ACCURACY_DOWN_HIT" },
[191] = { id = 191, name = "SPIKES", pp = 20, power = 0, accuracy = 0, type = "GROUND", priority = 0, effect = "SPIKES" },
[192] = { id = 192, name = "ZAP CANNON", pp = 5, power = 100, accuracy = 50, type = "ELECTRIC", priority = 0, effect = "PARALYZE_HIT" },
[193] = { id = 193, name = "FORESIGHT", pp = 40, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "FORESIGHT" },
[194] = { id = 194, name = "DESTINY BOND", pp = 5, power = 0, accuracy = 0, type = "GHOST", priority = 0, effect = "DESTINY_BOND" },
[195] = { id = 195, name = "PERISH SONG", pp = 5, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "PERISH_SONG" },
[196] = { id = 196, name = "ICY WIND", pp = 15, power = 55, accuracy = 95, type = "ICE", priority = 0, effect = "SPEED_DOWN_HIT" },
[197] = { id = 197, name = "DETECT", pp = 5, power = 0, accuracy = 0, type = "FIGHTING", priority = 3, effect = "PROTECT" },
[198] = { id = 198, name = "BONE RUSH", pp = 10, power = 25, accuracy = 80, type = "GROUND", priority = 0, effect = "MULTI_HIT" },
[199] = { id = 199, name = "LOCK-ON", pp = 5, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "LOCK_ON" },
[200] = { id = 200, name = "OUTRAGE", pp = 15, power = 90, accuracy = 100, type = "DRAGON", priority = 0, effect = "RAMPAGE" },
[201] = { id = 201, name = "SANDSTORM", pp = 10, power = 0, accuracy = 0, type = "ROCK", priority = 0, effect = "SANDSTORM" },
[202] = { id = 202, name = "GIGA DRAIN", pp = 5, power = 60, accuracy = 100, type = "GRASS", priority = 0, effect = "ABSORB" },
[203] = { id = 203, name = "ENDURE", pp = 10, power = 0, accuracy = 0, type = "NORMAL", priority = 3, effect = "ENDURE" },
[204] = { id = 204, name = "CHARM", pp = 20, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "ATTACK_DOWN_2" },
[205] = { id = 205, name = "ROLLOUT", pp = 20, power = 30, accuracy = 90, type = "ROCK", priority = 0, effect = "ROLLOUT" },
[206] = { id = 206, name = "FALSE SWIPE", pp = 40, power = 40, accuracy = 100, type = "NORMAL", priority = 0, effect = "FALSE_SWIPE" },
[207] = { id = 207, name = "SWAGGER", pp = 15, power = 0, accuracy = 90, type = "NORMAL", priority = 0, effect = "SWAGGER" },
[208] = { id = 208, name = "MILK DRINK", pp = 10, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "SOFTBOILED" },
[209] = { id = 209, name = "SPARK", pp = 20, power = 65, accuracy = 100, type = "ELECTRIC", priority = 0, effect = "PARALYZE_HIT" },
[210] = { id = 210, name = "FURY CUTTER", pp = 20, power = 10, accuracy = 95, type = "BUG", priority = 0, effect = "FURY_CUTTER" },
[211] = { id = 211, name = "STEEL WING", pp = 25, power = 70, accuracy = 90, type = "STEEL", priority = 0, effect = "DEFENSE_UP_HIT" },
[212] = { id = 212, name = "MEAN LOOK", pp = 5, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "MEAN_LOOK" },
[213] = { id = 213, name = "ATTRACT", pp = 15, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "ATTRACT" },
[214] = { id = 214, name = "SLEEP TALK", pp = 10, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "SLEEP_TALK" },
[215] = { id = 215, name = "HEAL BELL", pp = 5, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "HEAL_BELL" },
[216] = { id = 216, name = "RETURN", pp = 20, power = 1, accuracy = 100, type = "NORMAL", priority = 0, effect = "RETURN" },
[217] = { id = 217, name = "PRESENT", pp = 15, power = 1, accuracy = 90, type = "NORMAL", priority = 0, effect = "PRESENT" },
[218] = { id = 218, name = "FRUSTRATION", pp = 20, power = 1, accuracy = 100, type = "NORMAL", priority = 0, effect = "FRUSTRATION" },
[219] = { id = 219, name = "SAFEGUARD", pp = 25, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "SAFEGUARD" },
[220] = { id = 220, name = "PAIN SPLIT", pp = 20, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "PAIN_SPLIT" },
[221] = { id = 221, name = "SACRED FIRE", pp = 5, power = 100, accuracy = 95, type = "FIRE", priority = 0, effect = "THAW_HIT" },
[222] = { id = 222, name = "MAGNITUDE", pp = 30, power = 1, accuracy = 100, type = "GROUND", priority = 0, effect = "MAGNITUDE" },
[223] = { id = 223, name = "DYNAMICPUNCH", pp = 5, power = 100, accuracy = 50, type = "FIGHTING", priority = 0, effect = "CONFUSE_HIT" },
[224] = { id = 224, name = "MEGAHORN", pp = 10, power = 120, accuracy = 85, type = "BUG", priority = 0, effect = "HIT" },
[225] = { id = 225, name = "DRAGONBREATH", pp = 20, power = 60, accuracy = 100, type = "DRAGON", priority = 0, effect = "PARALYZE_HIT" },
[226] = { id = 226, name = "BATON PASS", pp = 40, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "BATON_PASS" },
[227] = { id = 227, name = "ENCORE", pp = 5, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "ENCORE" },
[228] = { id = 228, name = "PURSUIT", pp = 20, power = 40, accuracy = 100, type = "DARK", priority = 0, effect = "PURSUIT" },
[229] = { id = 229, name = "RAPID SPIN", pp = 40, power = 20, accuracy = 100, type = "NORMAL", priority = 0, effect = "RAPID_SPIN" },
[230] = { id = 230, name = "SWEET SCENT", pp = 20, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "EVASION_DOWN" },
[231] = { id = 231, name = "IRON TAIL", pp = 15, power = 100, accuracy = 75, type = "STEEL", priority = 0, effect = "DEFENSE_DOWN_HIT" },
[232] = { id = 232, name = "METAL CLAW", pp = 35, power = 50, accuracy = 95, type = "STEEL", priority = 0, effect = "ATTACK_UP_HIT" },
[233] = { id = 233, name = "VITAL THROW", pp = 10, power = 70, accuracy = 100, type = "FIGHTING", priority = -1, effect = "VITAL_THROW" },
[234] = { id = 234, name = "MORNING SUN", pp = 5, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "MORNING_SUN" },
[235] = { id = 235, name = "SYNTHESIS", pp = 5, power = 0, accuracy = 0, type = "GRASS", priority = 0, effect = "SYNTHESIS" },
[236] = { id = 236, name = "MOONLIGHT", pp = 5, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "MOONLIGHT" },
[237] = { id = 237, name = "HIDDEN POWER", pp = 15, power = 1, accuracy = 100, type = "NORMAL", priority = 0, effect = "HIDDEN_POWER" },
[238] = { id = 238, name = "CROSS CHOP", pp = 5, power = 100, accuracy = 80, type = "FIGHTING", priority = 0, effect = "HIGH_CRITICAL" },
[239] = { id = 239, name = "TWISTER", pp = 20, power = 40, accuracy = 100, type = "DRAGON", priority = 0, effect = "TWISTER" },
[240] = { id = 240, name = "RAIN DANCE", pp = 5, power = 0, accuracy = 0, type = "WATER", priority = 0, effect = "RAIN_DANCE" },
[241] = { id = 241, name = "SUNNY DAY", pp = 5, power = 0, accuracy = 0, type = "FIRE", priority = 0, effect = "SUNNY_DAY" },
[242] = { id = 242, name = "CRUNCH", pp = 15, power = 80, accuracy = 100, type = "DARK", priority = 0, effect = "SPECIAL_DEFENSE_DOWN_HIT" },
[243] = { id = 243, name = "MIRROR COAT", pp = 20, power = 1, accuracy = 100, type = "PSYCHIC", priority = -5, effect = "MIRROR_COAT" },
[244] = { id = 244, name = "PSYCH UP", pp = 10, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "PSYCH_UP" },
[245] = { id = 245, name = "EXTREMESPEED", pp = 5, power = 80, accuracy = 100, type = "NORMAL", priority = 1, effect = "QUICK_ATTACK" },
[246] = { id = 246, name = "ANCIENTPOWER", pp = 5, power = 60, accuracy = 100, type = "ROCK", priority = 0, effect = "ALL_STATS_UP_HIT" },
[247] = { id = 247, name = "SHADOW BALL", pp = 15, power = 80, accuracy = 100, type = "GHOST", priority = 0, effect = "SPECIAL_DEFENSE_DOWN_HIT" },
[248] = { id = 248, name = "FUTURE SIGHT", pp = 15, power = 80, accuracy = 90, type = "PSYCHIC", priority = 0, effect = "FUTURE_SIGHT" },
[249] = { id = 249, name = "ROCK SMASH", pp = 15, power = 20, accuracy = 100, type = "FIGHTING", priority = 0, effect = "DEFENSE_DOWN_HIT" },
[250] = { id = 250, name = "WHIRLPOOL", pp = 15, power = 15, accuracy = 70, type = "WATER", priority = 0, effect = "TRAP" },
[251] = { id = 251, name = "BEAT UP", pp = 10, power = 10, accuracy = 100, type = "DARK", priority = 0, effect = "BEAT_UP" },
[252] = { id = 252, name = "FAKE OUT", pp = 10, power = 40, accuracy = 100, type = "NORMAL", priority = 1, effect = "FAKE_OUT" },
[253] = { id = 253, name = "UPROAR", pp = 10, power = 50, accuracy = 100, type = "NORMAL", priority = 0, effect = "UPROAR" },
[254] = { id = 254, name = "STOCKPILE", pp = 10, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "STOCKPILE" },
[255] = { id = 255, name = "SPIT UP", pp = 10, power = 100, accuracy = 100, type = "NORMAL", priority = 0, effect = "SPIT_UP" },
[256] = { id = 256, name = "SWALLOW", pp = 10, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "SWALLOW" },
[257] = { id = 257, name = "HEAT WAVE", pp = 10, power = 100, accuracy = 90, type = "FIRE", priority = 0, effect = "BURN_HIT" },
[258] = { id = 258, name = "HAIL", pp = 10, power = 0, accuracy = 0, type = "ICE", priority = 0, effect = "HAIL" },
[259] = { id = 259, name = "TORMENT", pp = 15, power = 0, accuracy = 100, type = "DARK", priority = 0, effect = "TORMENT" },
[260] = { id = 260, name = "FLATTER", pp = 15, power = 0, accuracy = 100, type = "DARK", priority = 0, effect = "FLATTER" },
[261] = { id = 261, name = "WILL-O-WISP", pp = 15, power = 0, accuracy = 75, type = "FIRE", priority = 0, effect = "WILL_O_WISP" },
[262] = { id = 262, name = "MEMENTO", pp = 10, power = 0, accuracy = 100, type = "DARK", priority = 0, effect = "MEMENTO" },
[263] = { id = 263, name = "FACADE", pp = 20, power = 70, accuracy = 100, type = "NORMAL", priority = 0, effect = "FACADE" },
[264] = { id = 264, name = "FOCUS PUNCH", pp = 20, power = 150, accuracy = 100, type = "FIGHTING", priority = -3, effect = "FOCUS_PUNCH" },
[265] = { id = 265, name = "SMELLINGSALT", pp = 10, power = 60, accuracy = 100, type = "NORMAL", priority = 0, effect = "SMELLINGSALT" },
[266] = { id = 266, name = "FOLLOW ME", pp = 20, power = 0, accuracy = 100, type = "NORMAL", priority = 3, effect = "FOLLOW_ME" },
[267] = { id = 267, name = "NATURE POWER", pp = 20, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "NATURE_POWER" },
[268] = { id = 268, name = "CHARGE", pp = 20, power = 0, accuracy = 100, type = "ELECTRIC", priority = 0, effect = "CHARGE" },
[269] = { id = 269, name = "TAUNT", pp = 20, power = 0, accuracy = 100, type = "DARK", priority = 0, effect = "TAUNT" },
[270] = { id = 270, name = "HELPING HAND", pp = 20, power = 0, accuracy = 100, type = "NORMAL", priority = 5, effect = "HELPING_HAND" },
[271] = { id = 271, name = "TRICK", pp = 10, power = 0, accuracy = 100, type = "PSYCHIC", priority = 0, effect = "TRICK" },
[272] = { id = 272, name = "ROLE PLAY", pp = 10, power = 0, accuracy = 100, type = "PSYCHIC", priority = 0, effect = "ROLE_PLAY" },
[273] = { id = 273, name = "WISH", pp = 10, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "WISH" },
[274] = { id = 274, name = "ASSIST", pp = 20, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "ASSIST" },
[275] = { id = 275, name = "INGRAIN", pp = 20, power = 0, accuracy = 100, type = "GRASS", priority = 0, effect = "INGRAIN" },
[276] = { id = 276, name = "SUPERPOWER", pp = 5, power = 120, accuracy = 100, type = "FIGHTING", priority = 0, effect = "SUPERPOWER" },
[277] = { id = 277, name = "MAGIC COAT", pp = 15, power = 0, accuracy = 100, type = "PSYCHIC", priority = 4, effect = "MAGIC_COAT" },
[278] = { id = 278, name = "RECYCLE", pp = 10, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "RECYCLE" },
[279] = { id = 279, name = "REVENGE", pp = 10, power = 60, accuracy = 100, type = "FIGHTING", priority = -4, effect = "REVENGE" },
[280] = { id = 280, name = "BRICK BREAK", pp = 15, power = 75, accuracy = 100, type = "FIGHTING", priority = 0, effect = "BRICK_BREAK" },
[281] = { id = 281, name = "YAWN", pp = 10, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "YAWN" },
[282] = { id = 282, name = "KNOCK OFF", pp = 20, power = 20, accuracy = 100, type = "DARK", priority = 0, effect = "KNOCK_OFF" },
[283] = { id = 283, name = "ENDEAVOR", pp = 5, power = 1, accuracy = 100, type = "NORMAL", priority = 0, effect = "ENDEAVOR" },
[284] = { id = 284, name = "ERUPTION", pp = 5, power = 150, accuracy = 100, type = "FIRE", priority = 0, effect = "ERUPTION" },
[285] = { id = 285, name = "SKILL SWAP", pp = 10, power = 0, accuracy = 100, type = "PSYCHIC", priority = 0, effect = "SKILL_SWAP" },
[286] = { id = 286, name = "IMPRISON", pp = 10, power = 0, accuracy = 100, type = "PSYCHIC", priority = 0, effect = "IMPRISON" },
[287] = { id = 287, name = "REFRESH", pp = 20, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "REFRESH" },
[288] = { id = 288, name = "GRUDGE", pp = 5, power = 0, accuracy = 100, type = "GHOST", priority = 0, effect = "GRUDGE" },
[289] = { id = 289, name = "SNATCH", pp = 10, power = 0, accuracy = 100, type = "DARK", priority = 4, effect = "SNATCH" },
[290] = { id = 290, name = "SECRET POWER", pp = 20, power = 70, accuracy = 100, type = "NORMAL", priority = 0, effect = "SECRET_POWER" },
[291] = { id = 291, name = "DIVE", pp = 10, power = 60, accuracy = 100, type = "WATER", priority = 0, effect = "SEMI_INVULNERABLE" },
[292] = { id = 292, name = "ARM THRUST", pp = 20, power = 15, accuracy = 100, type = "FIGHTING", priority = 0, effect = "MULTI_HIT" },
[293] = { id = 293, name = "CAMOUFLAGE", pp = 20, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "CAMOUFLAGE" },
[294] = { id = 294, name = "TAIL GLOW", pp = 20, power = 0, accuracy = 100, type = "BUG", priority = 0, effect = "SPECIAL_ATTACK_UP_2" },
[295] = { id = 295, name = "LUSTER PURGE", pp = 5, power = 70, accuracy = 100, type = "PSYCHIC", priority = 0, effect = "SPECIAL_DEFENSE_DOWN_HIT" },
[296] = { id = 296, name = "MIST BALL", pp = 5, power = 70, accuracy = 100, type = "PSYCHIC", priority = 0, effect = "SPECIAL_ATTACK_DOWN_HIT" },
[297] = { id = 297, name = "FEATHERDANCE", pp = 15, power = 0, accuracy = 100, type = "FLYING", priority = 0, effect = "ATTACK_DOWN_2" },
[298] = { id = 298, name = "TEETER DANCE", pp = 20, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "TEETER_DANCE" },
[299] = { id = 299, name = "BLAZE KICK", pp = 10, power = 85, accuracy = 90, type = "FIRE", priority = 0, effect = "BLAZE_KICK" },
[300] = { id = 300, name = "MUD SPORT", pp = 15, power = 0, accuracy = 100, type = "GROUND", priority = 0, effect = "MUD_SPORT" },
[301] = { id = 301, name = "ICE BALL", pp = 20, power = 30, accuracy = 90, type = "ICE", priority = 0, effect = "ROLLOUT" },
[302] = { id = 302, name = "NEEDLE ARM", pp = 15, power = 60, accuracy = 100, type = "GRASS", priority = 0, effect = "FLINCH_MINIMIZE_HIT" },
[303] = { id = 303, name = "SLACK OFF", pp = 10, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "RESTORE_HP" },
[304] = { id = 304, name = "HYPER VOICE", pp = 10, power = 90, accuracy = 100, type = "NORMAL", priority = 0, effect = "HIT" },
[305] = { id = 305, name = "POISON FANG", pp = 15, power = 50, accuracy = 100, type = "POISON", priority = 0, effect = "POISON_FANG" },
[306] = { id = 306, name = "CRUSH CLAW", pp = 10, power = 75, accuracy = 95, type = "NORMAL", priority = 0, effect = "DEFENSE_DOWN_HIT" },
[307] = { id = 307, name = "BLAST BURN", pp = 5, power = 150, accuracy = 90, type = "FIRE", priority = 0, effect = "RECHARGE" },
[308] = { id = 308, name = "HYDRO CANNON", pp = 5, power = 150, accuracy = 90, type = "WATER", priority = 0, effect = "RECHARGE" },
[309] = { id = 309, name = "METEOR MASH", pp = 10, power = 100, accuracy = 85, type = "STEEL", priority = 0, effect = "ATTACK_UP_HIT" },
[310] = { id = 310, name = "ASTONISH", pp = 15, power = 30, accuracy = 100, type = "GHOST", priority = 0, effect = "FLINCH_MINIMIZE_HIT" },
[311] = { id = 311, name = "WEATHER BALL", pp = 10, power = 50, accuracy = 100, type = "NORMAL", priority = 0, effect = "WEATHER_BALL" },
[312] = { id = 312, name = "AROMATHERAPY", pp = 5, power = 0, accuracy = 0, type = "GRASS", priority = 0, effect = "HEAL_BELL" },
[313] = { id = 313, name = "FAKE TEARS", pp = 20, power = 0, accuracy = 100, type = "DARK", priority = 0, effect = "SPECIAL_DEFENSE_DOWN_2" },
[314] = { id = 314, name = "AIR CUTTER", pp = 25, power = 55, accuracy = 95, type = "FLYING", priority = 0, effect = "HIGH_CRITICAL" },
[315] = { id = 315, name = "OVERHEAT", pp = 5, power = 140, accuracy = 90, type = "FIRE", priority = 0, effect = "OVERHEAT" },
[316] = { id = 316, name = "ODOR SLEUTH", pp = 40, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "FORESIGHT" },
[317] = { id = 317, name = "ROCK TOMB", pp = 10, power = 50, accuracy = 80, type = "ROCK", priority = 0, effect = "SPEED_DOWN_HIT" },
[318] = { id = 318, name = "SILVER WIND", pp = 5, power = 60, accuracy = 100, type = "BUG", priority = 0, effect = "ALL_STATS_UP_HIT" },
[319] = { id = 319, name = "METAL SOUND", pp = 40, power = 0, accuracy = 85, type = "STEEL", priority = 0, effect = "SPECIAL_DEFENSE_DOWN_2" },
[320] = { id = 320, name = "GRASSWHISTLE", pp = 15, power = 0, accuracy = 55, type = "GRASS", priority = 0, effect = "SLEEP" },
[321] = { id = 321, name = "TICKLE", pp = 20, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "TICKLE" },
[322] = { id = 322, name = "COSMIC POWER", pp = 20, power = 0, accuracy = 0, type = "PSYCHIC", priority = 0, effect = "COSMIC_POWER" },
[323] = { id = 323, name = "WATER SPOUT", pp = 5, power = 150, accuracy = 100, type = "WATER", priority = 0, effect = "ERUPTION" },
[324] = { id = 324, name = "SIGNAL BEAM", pp = 15, power = 75, accuracy = 100, type = "BUG", priority = 0, effect = "CONFUSE_HIT" },
[325] = { id = 325, name = "SHADOW PUNCH", pp = 20, power = 60, accuracy = 0, type = "GHOST", priority = 0, effect = "ALWAYS_HIT" },
[326] = { id = 326, name = "EXTRASENSORY", pp = 30, power = 80, accuracy = 100, type = "PSYCHIC", priority = 0, effect = "FLINCH_MINIMIZE_HIT" },
[327] = { id = 327, name = "SKY UPPERCUT", pp = 15, power = 85, accuracy = 90, type = "FIGHTING", priority = 0, effect = "SKY_UPPERCUT" },
[328] = { id = 328, name = "SAND TOMB", pp = 15, power = 15, accuracy = 70, type = "GROUND", priority = 0, effect = "TRAP" },
[329] = { id = 329, name = "SHEER COLD", pp = 5, power = 1, accuracy = 30, type = "ICE", priority = 0, effect = "OHKO" },
[330] = { id = 330, name = "MUDDY WATER", pp = 10, power = 95, accuracy = 85, type = "WATER", priority = 0, effect = "ACCURACY_DOWN_HIT" },
[331] = { id = 331, name = "BULLET SEED", pp = 30, power = 10, accuracy = 100, type = "GRASS", priority = 0, effect = "MULTI_HIT" },
[332] = { id = 332, name = "AERIAL ACE", pp = 20, power = 60, accuracy = 0, type = "FLYING", priority = 0, effect = "ALWAYS_HIT" },
[333] = { id = 333, name = "ICICLE SPEAR", pp = 30, power = 10, accuracy = 100, type = "ICE", priority = 0, effect = "MULTI_HIT" },
[334] = { id = 334, name = "IRON DEFENSE", pp = 15, power = 0, accuracy = 0, type = "STEEL", priority = 0, effect = "DEFENSE_UP_2" },
[335] = { id = 335, name = "BLOCK", pp = 5, power = 0, accuracy = 100, type = "NORMAL", priority = 0, effect = "MEAN_LOOK" },
[336] = { id = 336, name = "HOWL", pp = 40, power = 0, accuracy = 0, type = "NORMAL", priority = 0, effect = "ATTACK_UP" },
[337] = { id = 337, name = "DRAGON CLAW", pp = 15, power = 80, accuracy = 100, type = "DRAGON", priority = 0, effect = "HIT" },
[338] = { id = 338, name = "FRENZY PLANT", pp = 5, power = 150, accuracy = 90, type = "GRASS", priority = 0, effect = "RECHARGE" },
[339] = { id = 339, name = "BULK UP", pp = 20, power = 0, accuracy = 0, type = "FIGHTING", priority = 0, effect = "BULK_UP" },
[340] = { id = 340, name = "BOUNCE", pp = 5, power = 85, accuracy = 85, type = "FLYING", priority = 0, effect = "SEMI_INVULNERABLE" },
[341] = { id = 341, name = "MUD SHOT", pp = 15, power = 55, accuracy = 95, type = "GROUND", priority = 0, effect = "SPEED_DOWN_HIT" },
[342] = { id = 342, name = "POISON TAIL", pp = 25, power = 50, accuracy = 100, type = "POISON", priority = 0, effect = "POISON_TAIL" },
[343] = { id = 343, name = "COVET", pp = 40, power = 40, accuracy = 100, type = "NORMAL", priority = 0, effect = "THIEF" },
[344] = { id = 344, name = "VOLT TACKLE", pp = 15, power = 120, accuracy = 100, type = "ELECTRIC", priority = 0, effect = "DOUBLE_EDGE" },
[345] = { id = 345, name = "MAGICAL LEAF", pp = 20, power = 60, accuracy = 0, type = "GRASS", priority = 0, effect = "ALWAYS_HIT" },
[346] = { id = 346, name = "WATER SPORT", pp = 15, power = 0, accuracy = 100, type = "WATER", priority = 0, effect = "WATER_SPORT" },
[347] = { id = 347, name = "CALM MIND", pp = 20, power = 0, accuracy = 0, type = "PSYCHIC", priority = 0, effect = "CALM_MIND" },
[348] = { id = 348, name = "LEAF BLADE", pp = 15, power = 70, accuracy = 100, type = "GRASS", priority = 0, effect = "HIGH_CRITICAL" },
[349] = { id = 349, name = "DRAGON DANCE", pp = 20, power = 0, accuracy = 0, type = "DRAGON", priority = 0, effect = "DRAGON_DANCE" },
[350] = { id = 350, name = "ROCK BLAST", pp = 10, power = 25, accuracy = 80, type = "ROCK", priority = 0, effect = "MULTI_HIT" },
[351] = { id = 351, name = "SHOCK WAVE", pp = 20, power = 60, accuracy = 0, type = "ELECTRIC", priority = 0, effect = "ALWAYS_HIT" },
[352] = { id = 352, name = "WATER PULSE", pp = 20, power = 60, accuracy = 100, type = "WATER", priority = 0, effect = "CONFUSE_HIT" },
[353] = { id = 353, name = "DOOM DESIRE", pp = 5, power = 120, accuracy = 85, type = "STEEL", priority = 0, effect = "FUTURE_SIGHT" },
[354] = { id = 354, name = "PSYCHO BOOST", pp = 5, power = 140, accuracy = 90, type = "PSYCHIC", priority = 0, effect = "OVERHEAT" },
}
return BuiltinMoves
+9 -3
View File
@@ -31,9 +31,15 @@ local function diskFallback(rel)
local identity = os.getenv("POKEPORT_IDENTITY") or ""
local sandboxed = identity ~= ""
local home = os.getenv("HOME")
if home and sandboxed then
roots[#roots + 1] = home .. "/Library/Application Support/LOVE/" .. identity
roots[#roots + 1] = home .. "/.local/share/love/" .. identity
if home then
if sandboxed then
roots[#roots + 1] = home .. "/Library/Application Support/LOVE/" .. identity
roots[#roots + 1] = home .. "/.local/share/love/" .. identity
else
roots[#roots + 1] = home .. "/Library/Application Support/LOVE/pokemon-love2d"
roots[#roots + 1] = home .. "/.local/share/love/pokemon-love2d"
roots[#roots + 1] = home .. "/.local/share/love/Gen2Recomp"
end
end
if love and love.filesystem and love.filesystem.getSaveDirectory then
local sd = love.filesystem.getSaveDirectory()
+13 -2
View File
@@ -894,7 +894,11 @@ function Pokemon.moveName(moveId)
if not Pokemon._moveNames then Pokemon.install(Pokemon._cache) end
local n = Pokemon._moveNames and Pokemon._moveNames[num]
if n and n ~= "" then return n end
error("no ROM move name for move " .. num, 2)
local okB, BuiltinMoves = pcall(require, "src.core.game3.battle.builtin_moves")
if okB and BuiltinMoves and BuiltinMoves[num] and BuiltinMoves[num].name then
return BuiltinMoves[num].name
end
return "MOVE " .. tostring(num)
end
function Pokemon.learnset(species)
@@ -943,7 +947,14 @@ function Pokemon.battleMove(moveId)
moveId = tonumber(moveId)
if not moveId then return nil end
if not Pokemon._battleMoves then Pokemon.install(Pokemon._cache) end
return Pokemon._battleMoves and Pokemon._battleMoves[moveId]
if Pokemon._battleMoves and Pokemon._battleMoves[moveId] then
return Pokemon._battleMoves[moveId]
end
local okB, BuiltinMoves = pcall(require, "src.core.game3.battle.builtin_moves")
if okB and BuiltinMoves and BuiltinMoves[moveId] then
return BuiltinMoves[moveId]
end
return nil
end
function Pokemon.movePp(moveId)
+1
View File
@@ -139,6 +139,7 @@ return {
"egg_extract",
"battle_anim_extract",
"battle_ai_extract",
"map_preview_extract",
"credits_extract",
"league_extract",
},
+68 -17
View File
@@ -76,6 +76,14 @@ local function resolveMkdir()
local resolved = pcall(function() return ffi.C.mkdir end)
if resolved then
mkdirFn = function(path) pcall(ffi.C.mkdir, path, 493) end -- 0755
else
local okl, clib = pcall(ffi.load, "c")
if okl and clib then
local okm = pcall(function() return clib.mkdir end)
if okm then
mkdirFn = function(path) pcall(clib.mkdir, path, 493) end
end
end
end
end
return mkdirFn
@@ -254,27 +262,67 @@ local function realPath(root, rel)
return root .. SEP .. rel:gsub("/", SEP)
end
local knownDirs = {}
-- create every parent directory of `rel` under `root` (best effort; an
-- already-existing directory is fine, a genuine failure surfaces when the
-- subsequent io.open write fails)
local function ensureParents(root, rel)
local mkdir = resolveMkdir()
if not mkdir then return end
local parts = {}
for part in rel:gmatch("[^/]+") do parts[#parts + 1] = part end
local cur = root
local curRel = ""
for i = 1, #parts - 1 do
cur = cur .. SEP .. parts[i]
mkdir(cur)
curRel = (curRel == "" and parts[i]) or (curRel .. "/" .. parts[i])
if not knownDirs[cur] then
if mkdir then
mkdir(cur)
elseif love and love.filesystem and love.filesystem.createDirectory then
love.filesystem.createDirectory(curRel)
end
knownDirs[cur] = true
end
end
end
local function resolveWriteRoot()
local root = CacheFs.root()
if root then return root end
if Platform.isNX and Platform.isNX() then return nil end
if Platform.isUWP and Platform.isUWP() then return nil end
if love and (love._version or love.getVersion) and love.filesystem and love.filesystem.getSaveDirectory then
local saveDir = love.filesystem.getSaveDirectory()
if saveDir and type(saveDir) == "string" and saveDir ~= "" and not unsafe_rel(saveDir) then
return saveDir
end
end
return nil
end
local function ensureDirectory(parent)
if not parent or parent == "" or knownDirs[parent] then return true end
if not (love and love.filesystem and love.filesystem.createDirectory) then return true end
if love.filesystem.createDirectory(parent) then
knownDirs[parent] = true
return true
end
local info = love.filesystem.getInfo and love.filesystem.getInfo(parent)
if info and info.type == "directory" then
knownDirs[parent] = true
return true
end
local reason = info and ("a " .. info.type .. " already exists there") or "unknown reason"
return false, "could not create " .. parent .. ": " .. reason
end
-- write cache-relative `rel` (forward-slash path) with the given bytes;
-- returns ok, err like love.filesystem.write
function CacheFs.write(rel, data)
rel = withPrefix(rel)
if unsafe_rel(rel) then return false, "unsafe cache path" end
local root = CacheFs.root()
local root = resolveWriteRoot()
if root then
ensureParents(root, rel)
local f, err = io.open(realPath(root, rel), "wb")
@@ -284,11 +332,9 @@ function CacheFs.write(rel, data)
return true
end
local parent = rel:match("^(.*)/[^/]+$")
if parent and not love.filesystem.createDirectory(parent) then
local info = love.filesystem.getInfo(parent)
local reason = info and ("a " .. info.type .. " already exists there")
or "unknown reason"
return false, "could not create " .. parent .. ": " .. reason
if parent then
local okDir, dirErr = ensureDirectory(parent)
if not okDir then return false, dirErr end
end
return love.filesystem.write(rel, data)
end
@@ -300,7 +346,7 @@ end
function CacheFs.openWrite(rel)
rel = withPrefix(rel)
if unsafe_rel(rel) then return nil, "unsafe cache path" end
local root = CacheFs.root()
local root = resolveWriteRoot()
if root then
ensureParents(root, rel)
local f, err = io.open(realPath(root, rel), "wb")
@@ -318,11 +364,9 @@ function CacheFs.openWrite(rel)
return nil, "streaming cache writes are unavailable"
end
local parent = rel:match("^(.*)/[^/]+$")
if parent and not love.filesystem.createDirectory(parent) then
local info = love.filesystem.getInfo(parent)
local reason = info and ("a " .. info.type .. " already exists there")
or "unknown reason"
return nil, "could not create " .. parent .. ": " .. reason
if parent then
local okDir, dirErr = ensureDirectory(parent)
if not okDir then return nil, dirErr end
end
local file, makeErr = love.filesystem.newFile(rel)
if not file then return nil, makeErr or "could not create cache file" end
@@ -408,7 +452,10 @@ function CacheFs.existsAt(rel)
f:close()
return true
end
return love.filesystem.getInfo(rel, "file") ~= nil
if love and love.filesystem and love.filesystem.getInfo then
return love.filesystem.getInfo(rel, "file") ~= nil
end
return false
end
@@ -425,7 +472,9 @@ function CacheFs.remove(rel)
os.remove(realPath(root, rel))
return
end
love.filesystem.remove(rel)
if love and love.filesystem and love.filesystem.remove then
love.filesystem.remove(rel)
end
end
-- Remove a single cache-relative directory once its files are gone. Needed
@@ -443,7 +492,9 @@ function CacheFs.removeDir(rel)
if rmdir then rmdir(realPath(root, rel)) end
return
end
love.filesystem.remove(rel)
if love and love.filesystem and love.filesystem.remove then
love.filesystem.remove(rel)
end
end
-- Remove the game-folder copy of a cache subtree before a fresh import, so a
+21 -8
View File
@@ -7,17 +7,30 @@
-- this only fills the tree between those two steps, so the "marker appears
-- last" order isReady() depends on stays on one thread.
require("love.filesystem")
require("love.image")
require("love.math")
require("love.system")
require("love.timer")
require("love.data")
pcall(require, "love.filesystem")
pcall(require, "love.image")
pcall(require, "love.math")
pcall(require, "love.system")
pcall(require, "love.timer")
pcall(require, "love.data")
pcall(require, "love.thread")
-- Ensure fresh thread Lua state resolves "src.*" via love.filesystem on all platforms:
table.insert(package.searchers or package.loaders, 1, function(modname)
local path = modname:gsub("%.", "/") .. ".lua"
if love and love.filesystem and love.filesystem.getInfo and love.filesystem.getInfo(path) then
return assert(love.filesystem.load(path))
end
local initPath = modname:gsub("%.", "/") .. "/init.lua"
if love and love.filesystem and love.filesystem.getInfo and love.filesystem.getInfo(initPath) then
return assert(love.filesystem.load(initPath))
end
end)
local version, prefix, romData, progressName, resultName, romSha1 = ...
local progressChannel = love.thread.getChannel(progressName)
local resultChannel = love.thread.getChannel(resultName)
local progressChannel = love.thread and love.thread.getChannel and love.thread.getChannel(progressName)
local resultChannel = love.thread and love.thread.getChannel and love.thread.getChannel(resultName)
-- RomExtractor:tick fires per item, thousands of times per import; a channel
-- push each would cost more than the work it reports.
+153 -67
View File
@@ -83,22 +83,20 @@ local function makeCache()
end
local POKEMON_SUBTASKS = {
["pokemon"] = { min = 0.00, max = 0.40, label = "Pokémon Species & Sprites" },
["learnsets"] = { min = 0.40, max = 0.55, label = "Move Learnsets" },
["battle_moves"] = { min = 0.55, max = 0.60, label = "Battle Moves Data" },
["party_chrome"] = { min = 0.60, max = 0.65, label = "Party UI Graphics" },
["battle_chrome"] = { min = 0.65, max = 0.70, label = "Battle UI Graphics" },
["pokedex_entries"] = { min = 0.70, max = 0.73, label = "Pokédex Database" },
["pokedex_categories"]= { min = 0.73, max = 0.75, label = "Pokédex Categories" },
["pokedex_orders"] = { min = 0.75, max = 0.77, label = "Pokédex Sorting" },
["pokedex_done"] = { min = 0.77, max = 0.78, label = "Pokédex Complete" },
["storage_chrome"] = { min = 0.78, max = 0.82, label = "PC Storage Chrome" },
["battle_transition"] = { min = 0.82, max = 0.86, label = "Battle Transitions" },
["summary_chrome"] = { min = 0.86, max = 0.90, label = "Summary Screen Graphics" },
["bag_chrome"] = { min = 0.90, max = 0.94, label = "Bag & Items Graphics" },
["shop_chrome"] = { min = 0.94, max = 0.97, label = "Mart & Shop Graphics" },
["trainers"] = { min = 0.97, max = 0.99, label = "Trainer Data & Parties" },
["map_preview"] = { min = 0.99, max = 1.00, label = "Location Previews" },
["pokemon"] = { min = 0.00, max = 0.50, label = "Pokémon Species & Sprites" },
["learnsets"] = { min = 0.50, max = 0.58, label = "Move Learnsets" },
["battle_moves"] = { min = 0.58, max = 0.62, label = "Battle Moves Data" },
["party_chrome"] = { min = 0.62, max = 0.68, label = "Party UI Graphics" },
["battle_chrome"] = { min = 0.68, max = 0.74, label = "Battle UI Graphics" },
["pokedex_entries"] = { min = 0.74, max = 0.78, label = "Pokédex Database" },
["pokedex_categories"]= { min = 0.78, max = 0.80, label = "Pokédex Categories" },
["pokedex_orders"] = { min = 0.80, max = 0.82, label = "Pokédex Sorting" },
["pokedex_done"] = { min = 0.82, max = 0.84, label = "Pokédex Complete" },
["storage_chrome"] = { min = 0.84, max = 0.88, label = "PC Storage Chrome" },
["battle_transition"] = { min = 0.88, max = 0.92, label = "Battle Transitions" },
["summary_chrome"] = { min = 0.92, max = 0.96, label = "Summary Screen Graphics" },
["bag_chrome"] = { min = 0.96, max = 0.98, label = "Bag & Items Graphics" },
["shop_chrome"] = { min = 0.98, max = 1.00, label = "Mart & Shop Graphics" },
}
function RomExtractorGen3.new(romData, manifest, progressCb, romSha1)
@@ -204,9 +202,43 @@ function RomExtractorGen3:writeRequiredMarkers(sha1)
if not CacheFs.exists("data/generated/audio.lua") then
LuaWriter.write("data/generated/audio.lua", { stub = true })
end
local dirs = {
GBA_ROOT,
GBA_ROOT .. "/pokemon",
GBA_ROOT .. "/pokemon/front",
GBA_ROOT .. "/pokemon/back",
GBA_ROOT .. "/pokemon/front_shiny",
GBA_ROOT .. "/pokemon/back_shiny",
GBA_ROOT .. "/pokemon/icons",
GBA_ROOT .. "/pokemon/party",
GBA_ROOT .. "/pokemon/storage",
GBA_ROOT .. "/pokemon/battle",
GBA_ROOT .. "/pokemon/pokedex",
GBA_ROOT .. "/region_map",
GBA_ROOT .. "/map_preview",
GBA_ROOT .. "/items",
GBA_ROOT .. "/items/bag",
GBA_ROOT .. "/items/shop",
GBA_ROOT .. "/items/tm_case",
GBA_ROOT .. "/items/berry_pouch",
GBA_ROOT .. "/battle_ai",
GBA_ROOT .. "/intro",
GBA_ROOT .. "/audio",
GBA_ROOT .. "/chrome",
GBA_ROOT .. "/chrome/fonts",
GBA_ROOT .. "/ow",
GBA_ROOT .. "/native",
}
for _, d in ipairs(dirs) do
if love and love.filesystem and love.filesystem.createDirectory then
local p = (CacheFs.prefix or "") .. d
pcall(love.filesystem.createDirectory, p)
end
end
end
function RomExtractorGen3:runGbaExtract(sha1)
function RomExtractorGen3:runGbaExtract(sha1, skipScriptsAndOw)
local Extract = require("src.import.gba.extract_island1")
local prevRoot, prevNative = Extract.CACHE_ROOT, Extract.NATIVE_ROOT
Extract.CACHE_ROOT = GBA_ROOT
@@ -224,7 +256,7 @@ function RomExtractorGen3:runGbaExtract(sha1)
label = label .. string.format(" (%d/%d)", cur or 0, total)
end
self:report(math.min(frac, 1.0), label, cur or 0, total or 1)
end)
end, { skipScriptsAndOw = skipScriptsAndOw == true })
end)
Extract.CACHE_ROOT = prevRoot
@@ -236,8 +268,29 @@ function RomExtractorGen3:runGbaExtract(sha1)
return runOk, runDetail
end
function RomExtractorGen3:runScriptsAndOwExtract(sha1)
local Extract = require("src.import.gba.extract_island1")
local prevRoot = Extract.CACHE_ROOT
Extract.CACHE_ROOT = GBA_ROOT
local imports = self:sharedImports(sha1)
local cache = makeCache()
local runOk, runDetail = false, "scripts_ow did not run"
local callOk, err = pcall(function()
runOk = Extract.runScriptsAndOw(imports, cache, function(cur, total, stageName)
local frac = (cur or 0) / math.max(total or 4, 1)
self:report(frac, "Scripts & OW: " .. tostring(stageName or "Processing"), cur or 0, total or 4)
end)
return runOk
end)
Extract.CACHE_ROOT = prevRoot
if not callOk then return false, err end
return runOk, runDetail
end
--- Species pack + party chrome into data/generated/gba/pokemon/.
function RomExtractorGen3:runPokemonExtract(sha1)
function RomExtractorGen3:runPokemonExtract(sha1, spMin, spMax)
local Rom = require("src.import.gba.rom")
local PokemonExtract = require("src.import.gba.pokemon_extract")
local Extract = require("src.import.gba.extract_island1")
@@ -245,7 +298,7 @@ function RomExtractorGen3:runPokemonExtract(sha1)
Extract.CACHE_ROOT = GBA_ROOT
local cache = makeCache()
if PokemonExtract.ready(cache, GBA_ROOT) then
if not spMin and PokemonExtract.ready(cache, GBA_ROOT) then
Extract.CACHE_ROOT = prevRoot
return true, { skipped = true }
end
@@ -261,37 +314,54 @@ function RomExtractorGen3:runPokemonExtract(sha1)
local ok, detail = pcall(function()
local pRes = PokemonExtract.run(rom, cache, {
cacheRoot = GBA_ROOT,
spMin = spMin or 0,
spMax = spMax,
progress = function(name, cur, total)
self:tickPokemon(name or "pokemon", cur or 0, total or 1)
end,
})
local ItemsExtract = require("src.import.gba.items_extract")
ItemsExtract.run(rom, cache, { cacheRoot = GBA_ROOT })
local PokedexExtract = require("src.import.gba.pokedex_chrome_extract")
PokedexExtract.run(rom, cache, { cacheRoot = GBA_ROOT })
local StorageExtract = require("src.import.gba.storage_chrome_extract")
StorageExtract.run(rom, cache, { cacheRoot = GBA_ROOT })
local TextChromeExtract = require("src.import.gba.text_chrome_extract")
TextChromeExtract.run(rom, cache, { cacheRoot = GBA_ROOT })
local TrainerCardExtract = require("src.import.gba.trainer_card_extract")
TrainerCardExtract.run(rom, cache, { cacheRoot = GBA_ROOT })
local SeagallopExtract = require("src.import.gba.seagallop_extract")
SeagallopExtract.run(rom, cache, { cacheRoot = GBA_ROOT })
require("src.import.gba.cave_transition_extract").run(rom, cache, { cacheRoot = GBA_ROOT })
require("src.import.gba.weather_extract").run(rom, cache, { cacheRoot = GBA_ROOT })
require("src.import.gba.ingame_trades_extract").run(rom, cache, { cacheRoot = GBA_ROOT })
require("src.import.gba.union_room_classes_extract").run(rom, cache, { cacheRoot = GBA_ROOT })
local MapPreviewExtract = require("src.import.gba.map_preview_extract")
local mpOk, mpErr = pcall(MapPreviewExtract.run, rom, cache, {
return pRes
end)
Extract.CACHE_ROOT = prevRoot
if not ok then
return false, detail
end
return true, detail
end
function RomExtractorGen3:runPokemonGfxExtract(sha1, spMin, spMax)
local Rom = require("src.import.gba.rom")
local PokemonExtract = require("src.import.gba.pokemon_extract")
local Extract = require("src.import.gba.extract_island1")
local prevRoot = Extract.CACHE_ROOT
Extract.CACHE_ROOT = GBA_ROOT
local imports = self:sharedImports(sha1)
local version = importVersion(sha1)
local rom, openErr = Rom.open(imports, version)
if not rom then
Extract.CACHE_ROOT = prevRoot
return false, openErr or "rom open failed"
end
local cache = makeCache()
local ok, detail = pcall(function()
return PokemonExtract.run(rom, cache, {
cacheRoot = GBA_ROOT,
spMin = spMin or 0,
spMax = spMax or 200,
onlySpeciesGfx = true,
progress = function(name, cur, total)
self:tickPokemon(name or "map_preview", cur or 0, total or 1)
self:tickPokemon(name or "pokemon", cur or 0, total or 1)
end,
})
if not mpOk then
print("[map_preview] warn: " .. tostring(mpErr))
end
return pRes
end)
Extract.CACHE_ROOT = prevRoot
@@ -339,32 +409,34 @@ function RomExtractorGen3:runAuxExtracts(sha1)
Extract.CACHE_ROOT = GBA_ROOT
local cache = makeCache()
local needRegion = wantedExtractor("region_map_extract") and not RegionMapExtract.ready(cache, GBA_ROOT)
local needRegion = wantedExtractor("region_map_extract") and not (RegionMapExtract.ready and RegionMapExtract.ready(cache, GBA_ROOT))
local needSections = wantedExtractor("map_sections_extract")
and not CacheFs.exists(GBA_ROOT .. "/region_map/map_sections.lua")
local needChoices = wantedExtractor("multichoice_extract") and not MultichoiceExtract.ready(cache, GBA_ROOT)
local needHeal = wantedExtractor("heal_locations_extract") and not HealLocationsExtract.ready(cache, GBA_ROOT)
local needDoors = wantedExtractor("door_anim_extract") and not DoorAnimExtract.ready(cache, GBA_ROOT)
local needSlots = wantedExtractor("slot_machine_extract") and not SlotMachineExtract.ready(cache, GBA_ROOT)
local needTrade = wantedExtractor("trade_extract") and not TradeExtract.ready(cache, GBA_ROOT)
local needLinkArt = wantedExtractor("link_art_extract") and not LinkArtExtract.ready(cache, GBA_ROOT)
local needFame = wantedExtractor("fame_checker_extract") and not FameCheckerExtract.ready(cache, GBA_ROOT)
local needTeachy = wantedExtractor("teachy_tv_extract") and not TeachyTvExtract.ready(cache, GBA_ROOT)
local needGift = wantedExtractor("mystery_gift_extract") and not MysteryGiftExtract.ready(cache, GBA_ROOT)
local needTower = wantedExtractor("trainer_tower_extract") and not TrainerTowerExtract.ready(cache, GBA_ROOT)
local needTutor = wantedExtractor("tutor_extract") and not TutorExtract.ready(cache, GBA_ROOT)
local needMuseum = wantedExtractor("museum_extract") and not MuseumExtract.ready(cache, GBA_ROOT)
local needRelearner = wantedExtractor("move_relearner_extract") and not MoveRelearnerExtract.ready(cache, GBA_ROOT)
local needEgg = wantedExtractor("egg_extract") and not EggExtract.ready(cache, GBA_ROOT)
local needAnims = wantedExtractor("battle_anim_extract") and not BattleAnimExtract.ready(cache, GBA_ROOT)
local needAi = wantedExtractor("battle_ai_extract") and not BattleAiExtract.ready(cache, GBA_ROOT)
local needCredits = wantedExtractor("credits_extract") and not CreditsExtract.ready(cache, GBA_ROOT)
local needLeague = wantedExtractor("league_extract") and not LeagueExtract.ready(cache, GBA_ROOT)
local needChoices = wantedExtractor("multichoice_extract") and not (MultichoiceExtract.ready and MultichoiceExtract.ready(cache, GBA_ROOT))
local needHeal = wantedExtractor("heal_locations_extract") and not (HealLocationsExtract.ready and HealLocationsExtract.ready(cache, GBA_ROOT))
local needDoors = wantedExtractor("door_anim_extract") and not (DoorAnimExtract.ready and DoorAnimExtract.ready(cache, GBA_ROOT))
local needSlots = wantedExtractor("slot_machine_extract") and not (SlotMachineExtract.ready and SlotMachineExtract.ready(cache, GBA_ROOT))
local needTrade = wantedExtractor("trade_extract") and not (TradeExtract.ready and TradeExtract.ready(cache, GBA_ROOT))
local needLinkArt = wantedExtractor("link_art_extract") and not (LinkArtExtract.ready and LinkArtExtract.ready(cache, GBA_ROOT))
local needFame = wantedExtractor("fame_checker_extract") and not (FameCheckerExtract.ready and FameCheckerExtract.ready(cache, GBA_ROOT))
local needTeachy = wantedExtractor("teachy_tv_extract") and not (TeachyTvExtract.ready and TeachyTvExtract.ready(cache, GBA_ROOT))
local needGift = wantedExtractor("mystery_gift_extract") and not (MysteryGiftExtract.ready and MysteryGiftExtract.ready(cache, GBA_ROOT))
local needTower = wantedExtractor("trainer_tower_extract") and not (TrainerTowerExtract.ready and TrainerTowerExtract.ready(cache, GBA_ROOT))
local needTutor = wantedExtractor("tutor_extract") and not (TutorExtract.ready and TutorExtract.ready(cache, GBA_ROOT))
local needMuseum = wantedExtractor("museum_extract") and not (MuseumExtract.ready and MuseumExtract.ready(cache, GBA_ROOT))
local needRelearner = wantedExtractor("move_relearner_extract") and not (MoveRelearnerExtract.ready and MoveRelearnerExtract.ready(cache, GBA_ROOT))
local needEgg = wantedExtractor("egg_extract") and not (EggExtract.ready and EggExtract.ready(cache, GBA_ROOT))
local needAnims = wantedExtractor("battle_anim_extract") and not (BattleAnimExtract.ready and BattleAnimExtract.ready(cache, GBA_ROOT))
local needAi = wantedExtractor("battle_ai_extract") and not (BattleAiExtract.ready and BattleAiExtract.ready(cache, GBA_ROOT))
local MapPreviewExtract = require("src.import.gba.map_preview_extract")
local needPreview = wantedExtractor("map_preview_extract") and not (MapPreviewExtract.ready and MapPreviewExtract.ready(cache, GBA_ROOT))
local needCredits = wantedExtractor("credits_extract") and not (CreditsExtract.ready and CreditsExtract.ready(cache, GBA_ROOT))
local needLeague = wantedExtractor("league_extract") and not (LeagueExtract.ready and LeagueExtract.ready(cache, GBA_ROOT))
if not (needRegion or needSections or needChoices or needHeal or needDoors
or needSlots or needTrade or needLinkArt or needFame or needTeachy
or needGift or needTower or needTutor or needMuseum or needRelearner
or needEgg or needAnims or needAi or needCredits or needLeague) then
or needEgg or needAnims or needAi or needPreview or needCredits or needLeague) then
Extract.CACHE_ROOT = prevRoot
return true, { skipped = true }
end
@@ -378,7 +450,7 @@ function RomExtractorGen3:runAuxExtracts(sha1)
local ok, detail = pcall(function()
local out = {}
local step = 0
local totalSteps = 19
local totalSteps = 20
local function auxTick(name)
step = step + 1
self:report(step / totalSteps, "Game Data: " .. name, step, totalSteps)
@@ -509,6 +581,13 @@ function RomExtractorGen3:runAuxExtracts(sha1)
end
auxTick("Battle AI Scripts")
if needPreview then
local mpOk, mpErr = pcall(MapPreviewExtract.run, rom, cache, { cacheRoot = GBA_ROOT })
if not mpOk then print("[map_preview] warn: " .. tostring(mpErr)) end
out.mapPreview = mpOk and mpErr or false
end
auxTick("Location Previews")
if needCredits then
out.credits = CreditsExtract.run(rom, cache, { cacheRoot = GBA_ROOT })
end
@@ -575,23 +654,26 @@ function RomExtractorGen3:runParallel(sha1)
local ch_name = "gba_extract_" .. tostring(love.timer.getTime()):gsub("%.", "") .. "_" .. tostring(math.random(10000, 99999))
local ch = love.thread.getChannel(ch_name)
local tasks = { "gba", "pokemon", "aux", "intro_audio" }
local threads = {}
local tasks = { "gba", "scripts_ow", "pokemon", "pokemon_gfx", "aux", "intro_audio" }
local prefix = CacheFs.prefix or ""
local workerCode = nil
if love and love.filesystem and love.filesystem.read then
workerCode = love.filesystem.read("src/import/gba/extract_worker.lua")
end
for _, t in ipairs(tasks) do
local okTh, th = pcall(love.thread.newThread, "src/import/gba/extract_worker.lua")
local okTh, th = pcall(love.thread.newThread, workerCode or "src/import/gba/extract_worker.lua")
if not okTh or not th then
return false, "failed to spawn thread for " .. t .. ": " .. tostring(th)
end
threads[t] = th
th:start(t, prefix, self.romData, sha1, ch_name)
end
local done_count = 0
local errors = {}
local task_progress = { gba = 0, pokemon = 0, aux = 0, intro_audio = 0 }
local weights = { gba = 0.40, pokemon = 0.35, aux = 0.15, intro_audio = 0.10 }
local task_progress = { gba = 0, scripts_ow = 0, pokemon = 0, pokemon_gfx = 0, aux = 0, intro_audio = 0 }
local weights = { gba = 0.20, scripts_ow = 0.20, pokemon = 0.20, pokemon_gfx = 0.15, aux = 0.15, intro_audio = 0.10 }
while done_count < #tasks do
local msg = ch:pop()
@@ -621,7 +703,7 @@ function RomExtractorGen3:runParallel(sha1)
end
if #errors > 0 then
error("Parallel extraction error:\n" .. table.concat(errors, "\n"))
return false, "Parallel extraction error:\n" .. table.concat(errors, "\n")
end
if not CacheFs.exists(GBA_ROOT .. "/maps.json") then
@@ -638,8 +720,10 @@ function RomExtractorGen3:run()
self:writeRequiredMarkers(sha1)
self:report(0.03, "Markers Ready", 1, 1)
local okPar, parErr = self:runParallel(sha1)
if okPar then
local okPar, parRes, parErr = pcall(function()
return self:runParallel(sha1)
end)
if okPar and parRes then
writeJson(GBA_ROOT .. "/pokemon/extract_status.json", { ok = true, error = nil })
writeJson(GBA_ROOT .. "/region_map/extract_status.json", { ok = true, error = nil })
self:report(1.00, "Ready", 1, 1)
@@ -652,6 +736,8 @@ function RomExtractorGen3:run()
}
end
print("[RomExtractorGen3] Parallel extraction fell back to sequential: " .. tostring(parErr or parRes))
-- Fallback sequential execution path
local ok, detail = self:runGbaExtract(sha1)
if ok then
+161 -29
View File
@@ -7,7 +7,7 @@ local Lz77 = require("src.import.gba.lz77")
local BagChromeExtract = {}
BagChromeExtract.CACHE_SUB = "items/bag"
BagChromeExtract.FORMAT_VERSION = 3
BagChromeExtract.FORMAT_VERSION = 4
BagChromeExtract.ITEM_PC_SUB = "items/item_pc"
local function default_cache_root()
@@ -28,16 +28,44 @@ local function bgr555_to_rgb8(c)
math.floor(b5 * 255 / 31 + 0.5)
end
local ffi
do
local ok, mod = pcall(require, "ffi")
if ok and mod and mod.new and mod.string then ffi = mod end
end
local bit = rawget(_G, "bit") or rawget(_G, "bit32")
if not bit then
local ok, mod = pcall(require, "bit")
if ok and mod then bit = mod end
end
local static_icon_buf = nil
local function get_icon_buf(size)
if not ffi then return nil end
if not static_icon_buf then
static_icon_buf = ffi.new("uint8_t[?]", math.max(size or 2304, 4096))
end
return static_icon_buf
end
local function byte_len(buf)
if type(buf) == "string" then return #buf end
if type(buf) ~= "table" then return 0 end
return buf._len or #buf
end
local function decode_tile_4bpp(tileBytes, out, baseX, baseY, stride, hflip, vflip)
local isStr = type(tileBytes) == "string"
for row = 0, 7 do
local srcRow = vflip and (7 - row) or row
for bx = 0, 3 do
local byte = tileBytes[srcRow * 4 + bx + 1] or 0
local byte
if isStr then
byte = string.byte(tileBytes, srcRow * 4 + bx + 1) or 0
else
byte = tileBytes[srcRow * 4 + bx + 1] or 0
end
local p0 = byte % 16
local p1 = math.floor(byte / 16) % 16
local x0 = bx * 2
@@ -53,13 +81,21 @@ end
local function load_pal_banks(bytes, count)
local banks = {}
local isStr = type(bytes) == "string"
local n = count or math.floor(byte_len(bytes) / 32)
for b = 0, n - 1 do
local colors = {}
local off = b * 32
for c = 0, 15 do
local i = off + c * 2 + 1
colors[c] = (bytes[i] or 0) + (bytes[i + 1] or 0) * 256
local b0, b1
if isStr then
b0 = string.byte(bytes, off + c * 2 + 1) or 0
b1 = string.byte(bytes, off + c * 2 + 2) or 0
else
b0 = bytes[off + c * 2 + 1] or 0
b1 = bytes[off + c * 2 + 2] or 0
end
colors[c] = b0 + b1 * 256
end
banks[b] = colors
end
@@ -67,6 +103,7 @@ local function load_pal_banks(bytes, count)
end
local function bake_tiles_rgba(gfx, banks, W, H, entryAt)
local isStr = type(gfx) == "string"
local tileCount = math.floor(byte_len(gfx) / 32)
local chunks = {}
local tmp = {}
@@ -80,7 +117,9 @@ local function bake_tiles_rgba(gfx, banks, W, H, entryAt)
if tileId >= tileCount then tileId = 0 end
local tile = {}
local base = tileId * 32
for i = 1, 32 do tile[i] = gfx[base + i] or 0 end
for i = 1, 32 do
tile[i] = isStr and (string.byte(gfx, base + i) or 0) or (gfx[base + i] or 0)
end
for i = 1, 64 do tmp[i] = 0 end
decode_tile_4bpp(tile, tmp, 0, 0, 8, hflip, vflip)
for row = 0, 7 do
@@ -97,6 +136,9 @@ end
local function map_entry(map, tx, ty)
local mi = (ty * 32 + tx) * 2 + 1
if type(map) == "string" then
return (string.byte(map, mi) or 0) + (string.byte(map, mi + 1) or 0) * 256
end
return (map[mi] or 0) + (map[mi + 1] or 0) * 256
end
@@ -119,10 +161,14 @@ local function bake_red_arrow(gfx, palBytes)
local pixels = {}
for i = 1, W * H do pixels[i] = 0 end
local ti = 0
local isStr = type(gfx) == "string"
for ty = 0, 3 do
for tx = 0, 1 do
local tile = {}
for i = 1, 32 do tile[i] = gfx[ti * 32 + i] or 0 end
local base = ti * 32
for i = 1, 32 do
tile[i] = isStr and (string.byte(gfx, base + i) or 0) or (gfx[base + i] or 0)
end
decode_tile_4bpp(tile, pixels, tx * 8, ty * 8, W, false, false)
ti = ti + 1
end
@@ -149,13 +195,16 @@ local function bake_bag_sprite(gfx, palBytes)
local pixels = {}
for i = 1, W * H do pixels[i] = 0 end
local ti = 0
local isStr = type(gfx) == "string"
for frame = 0, 3 do
for ty = 0, 7 do
for tx = 0, 7 do
if ti < tileCount then
local tile = {}
local base = ti * 32
for i = 1, 32 do tile[i] = gfx[base + i] or 0 end
for i = 1, 32 do
tile[i] = isStr and (string.byte(gfx, base + i) or 0) or (gfx[base + i] or 0)
end
decode_tile_4bpp(tile, pixels, tx * 8, frame * 64 + ty * 8, W, false, false)
end
ti = ti + 1
@@ -180,7 +229,63 @@ local function bake_icon_rgba(gfx, palBytes)
local W, H = 24, 24
local banks = load_pal_banks(palBytes, 1)
local pal = banks[0] or {}
local rgb = {}
for c = 0, 15 do
local r, g, b = bgr555_to_rgb8(pal[c] or 0)
rgb[c] = { r, g, b }
end
local total_bytes = W * H * 4
local buf = get_icon_buf(total_bytes)
local isStr = type(gfx) == "string"
local band = bit and bit.band
local rshift = bit and bit.rshift
local tileCount = math.floor(byte_len(gfx) / 32)
if buf and band and rshift then
local ti = 0
for ty = 0, 2 do
for tx = 0, 2 do
local tileOff = ti * 32
for row = 0, 7 do
for bx = 0, 3 do
local byte = 0
if ti < tileCount then
local bi = tileOff + row * 4 + bx
if isStr then
byte = string.byte(gfx, bi + 1) or 0
else
byte = gfx[bi + 1] or 0
end
end
local p0 = band(byte, 0x0F)
local p1 = rshift(byte, 4)
local x0 = tx * 8 + bx * 2
local y0 = ty * 8 + row
local offset0 = (y0 * W + x0) * 4
if p0 == 0 then
buf[offset0] = 0; buf[offset0 + 1] = 0; buf[offset0 + 2] = 0; buf[offset0 + 3] = 0
else
local c = rgb[p0] or rgb[0]
buf[offset0] = c[1]; buf[offset0 + 1] = c[2]; buf[offset0 + 2] = c[3]; buf[offset0 + 3] = 255
end
local offset1 = (y0 * W + x0 + 1) * 4
if p1 == 0 then
buf[offset1] = 0; buf[offset1 + 1] = 0; buf[offset1 + 2] = 0; buf[offset1 + 3] = 0
else
local c = rgb[p1] or rgb[0]
buf[offset1] = c[1]; buf[offset1 + 1] = c[2]; buf[offset1 + 2] = c[3]; buf[offset1 + 3] = 255
end
end
end
ti = ti + 1
end
end
return ffi.string(buf, total_bytes)
end
local pixels = {}
for i = 1, W * H do pixels[i] = 0 end
local ti = 0
@@ -189,7 +294,9 @@ local function bake_icon_rgba(gfx, palBytes)
if ti < tileCount then
local tile = {}
local base = ti * 32
for i = 1, 32 do tile[i] = gfx[base + i] or 0 end
for i = 1, 32 do
tile[i] = isStr and (string.byte(gfx, base + i) or 0) or (gfx[base + i] or 0)
end
decode_tile_4bpp(tile, pixels, tx * 8, ty * 8, W, false, false)
end
ti = ti + 1
@@ -214,11 +321,14 @@ local function bake_swap_line(gfx, palBytes)
local pal = load_pal_banks(palBytes, 1)[0] or {}
local pixels = {}
for i = 1, W * H do pixels[i] = 0 end
local isStr = type(gfx) == "string"
for frame = 0, 1 do
for t = 0, 3 do
local tile = {}
local base = (frame * 4 + t) * 32
for i = 1, 32 do tile[i] = gfx[base + i] or 0 end
for i = 1, 32 do
tile[i] = isStr and (string.byte(gfx, base + i) or 0) or (gfx[base + i] or 0)
end
decode_tile_4bpp(tile, pixels, frame * 16 + (t % 2) * 8, math.floor(t / 2) * 8, W, false, false)
end
end
@@ -250,19 +360,41 @@ function BagChromeExtract.run(rom, cache, opts)
local progress = opts.progress
local function get(i) return rom:get(i) end
local gfx = Lz77.decompress(get, Versions.BAG_BG_GFX)
local pal = Lz77.decompress(get, Versions.BAG_BG_PAL)
local map = Lz77.decompress(get, Versions.BAG_BG_TILEMAP)
local femalePal = Lz77.decompress(get, Versions.BAG_BG_PAL_FEMALE)
local function lz(off)
local ok, out = pcall(Lz77.decompressString, rom, off)
if ok and out then return out end
return Lz77.decompress(get, off)
end
local function readBytes(off, len)
if rom.readBytes then return rom:readBytes(off, len) end
local out = {}
for i = 1, len do out[i] = rom:get(off + i - 1) or 0 end
return out
end
local function u32(off)
if rom.u32 then return rom:u32(off) end
return (rom:get(off) or 0)
+ (rom:get(off + 1) or 0) * 256
+ (rom:get(off + 2) or 0) * 65536
+ (rom:get(off + 3) or 0) * 16777216
end
local gfx = lz(Versions.BAG_BG_GFX)
local pal = lz(Versions.BAG_BG_PAL)
local map = lz(Versions.BAG_BG_TILEMAP)
local femalePal = lz(Versions.BAG_BG_PAL_FEMALE)
local maleBanks = load_pal_banks(pal)
local femaleBanks = female_banks(pal, femalePal)
cache:write(root .. "/bg.rgba", bake_bg_rgba(gfx, maleBanks, map, W, H))
cache:write(root .. "/bg_female.rgba", bake_bg_rgba(gfx, femaleBanks, map, W, H))
local LW, LH = Versions.BAG_LIST_TILES_W, Versions.BAG_LIST_TILES_H
local listMap = rom:readBytes(Versions.BAG_LIST_TILEMAP, LW * LH * 2)
local listMap = readBytes(Versions.BAG_LIST_TILEMAP, LW * LH * 2)
local function list_entry(tx, ty)
local i = (ty * LW + tx) * 2 + 1
if type(listMap) == "string" then
return (string.byte(listMap, i) or 0) + (string.byte(listMap, i + 1) or 0) * 256
end
return (listMap[i] or 0) + (listMap[i + 1] or 0) * 256
end
local function blank_entry() return Versions.BAG_LIST_BLANK_TILE end
@@ -276,15 +408,15 @@ function BagChromeExtract.run(rom, cache, opts)
end))
-- src/item_menu.c:569
local pcMap = Lz77.decompress(get, Versions.BAG_BG_ITEM_PC_TILEMAP)
local pcMap = lz(Versions.BAG_BG_ITEM_PC_TILEMAP)
cache:write(root .. "/bg_itempc.rgba", bake_bg_rgba(gfx, maleBanks, pcMap, W, H))
cache:write(root .. "/bg_itempc_female.rgba", bake_bg_rgba(gfx, femaleBanks, pcMap, W, H))
-- src/item_pc.c:435 ItemPc_LoadGraphics, :748 ItemPc_SetMessageWindowPalette
local pcRoot = cacheRoot .. "/" .. BagChromeExtract.ITEM_PC_SUB
local ipcGfx = Lz77.decompress(get, Versions.ITEM_PC_TILES)
local ipcBanks = load_pal_banks(Lz77.decompress(get, Versions.ITEM_PC_BG_PALS), 3)
local ipcMap = Lz77.decompress(get, Versions.ITEM_PC_TILEMAP)
local ipcGfx = lz(Versions.ITEM_PC_TILES)
local ipcBanks = load_pal_banks(lz(Versions.ITEM_PC_BG_PALS), 3)
local ipcMap = lz(Versions.ITEM_PC_TILEMAP)
for name, msgPal in pairs({ bg = 1, bg_submenu = 2 }) do
cache:write(pcRoot .. "/" .. name .. ".rgba", bake_tiles_rgba(ipcGfx, ipcBanks, W, H, function(tx, ty)
local e = map_entry(ipcMap, tx, ty)
@@ -293,16 +425,16 @@ function BagChromeExtract.run(rom, cache, opts)
end))
end
cache:write(root .. "/swap_line.rgba", bake_swap_line(Lz77.decompress(get, Versions.BAG_SWAP_GFX),
Lz77.decompress(get, Versions.BAG_SWAP_PAL)))
cache:write(root .. "/swap_line.rgba", bake_swap_line(lz(Versions.BAG_SWAP_GFX),
lz(Versions.BAG_SWAP_PAL)))
local arrowGfx = Lz77.decompress(get, Versions.RED_ARROW_OTHER_GFX)
local arrowPal = rom:readBytes(Versions.RED_ARROW_PAL, 32)
local arrowGfx = lz(Versions.RED_ARROW_OTHER_GFX)
local arrowPal = readBytes(Versions.RED_ARROW_PAL, 32)
cache:write(root .. "/red_arrow.rgba", bake_red_arrow(arrowGfx, arrowPal))
local maleGfx = Lz77.decompress(get, Versions.BAG_MALE_GFX)
local femaleGfx = Lz77.decompress(get, Versions.BAG_FEMALE_GFX)
local sprPal = Lz77.decompress(get, Versions.BAG_SPRITE_PAL)
local maleGfx = lz(Versions.BAG_MALE_GFX)
local femaleGfx = lz(Versions.BAG_FEMALE_GFX)
local sprPal = lz(Versions.BAG_SPRITE_PAL)
local maleRgba, bw, bh, frames = bake_bag_sprite(maleGfx, sprPal)
local femaleRgba = bake_bag_sprite(femaleGfx, sprPal)
cache:write(root .. "/bag_male.rgba", maleRgba)
@@ -319,8 +451,8 @@ function BagChromeExtract.run(rom, cache, opts)
progress("icons", id, itemCount + 1)
end
local entryOff = tableOff + id * 8
local gfxPtr = rom:u32(entryOff)
local palPtr = rom:u32(entryOff + 4)
local gfxPtr = u32(entryOff)
local palPtr = u32(entryOff + 4)
local gfxOff = gba_to_file(gfxPtr)
local palOff = gba_to_file(palPtr)
if gfxOff and palOff then
@@ -328,8 +460,8 @@ function BagChromeExtract.run(rom, cache, opts)
local key = string.format("%08x:%08x", gfxPtr, palPtr)
local rgba = uniqGfx[key]
if not rgba then
local igfx = Lz77.decompress(get, gfxOff)
local ipal = Lz77.decompress(get, palOff)
local igfx = lz(gfxOff)
local ipal = lz(palOff)
rgba = bake_icon_rgba(igfx, ipal)
uniqGfx[key] = rgba
end
+24 -13
View File
@@ -535,19 +535,30 @@ local function encode_png(pixels, w, h)
if buf then
local dest = 0
for y = 0, h - 1 do
buf[dest] = 0 -- Filter: None
dest = dest + 1
local src_base = y * w * 4
if is_str then
for x = 0, w * 4 - 1 do
buf[dest] = pixels:byte(src_base + x + 1) or 0
dest = dest + 1
end
else
for x = 0, w * 4 - 1 do
buf[dest] = pixels[src_base + x + 1] or 0
dest = dest + 1
local row_bytes = w * 4
if is_str and ffi and ffi.copy then
local c_src = ffi.cast("const char*", pixels)
for y = 0, h - 1 do
buf[dest] = 0 -- Filter: None
dest = dest + 1
ffi.copy(buf + dest, c_src + (y * row_bytes), row_bytes)
dest = dest + row_bytes
end
else
for y = 0, h - 1 do
buf[dest] = 0 -- Filter: None
dest = dest + 1
local src_base = y * row_bytes
if is_str then
for x = 0, row_bytes - 1 do
buf[dest] = pixels:byte(src_base + x + 1) or 0
dest = dest + 1
end
else
for x = 0, row_bytes - 1 do
buf[dest] = pixels[src_base + x + 1] or 0
dest = dest + 1
end
end
end
end
+116 -144
View File
@@ -277,7 +277,7 @@ local MAP_ORDER = {
"FR_PEWTER_CITY_GYM",
}
function Extract.run(imports, cache, progressCb)
function Extract.run(imports, cache, progressCb, opts)
local importId, info = Extract.findImport(imports)
if not importId then
return false, "no FireRed/LeafGreen optional import installed"
@@ -477,33 +477,37 @@ function Extract.run(imports, cache, progressCb)
-- OW sprites + tileset anims + encounters + audio + chrome from ROM
do
local rom2 = assert(Rom.open(imports, importId))
require("src.import.gba.help_extract").writeExtract(rom2, cache)
require("src.import.gba.quest_log_extract").writeExtract(rom2, cache)
require("src.import.gba.object_interactions_extract").writeExtract(rom2, cache, Extract.CACHE_ROOT, version)
if not (opts and opts.skipScriptsAndOw) then
require("src.import.gba.help_extract").writeExtract(rom2, cache)
require("src.import.gba.quest_log_extract").writeExtract(rom2, cache)
require("src.import.gba.object_interactions_extract").writeExtract(rom2, cache, Extract.CACHE_ROOT, version)
end
local midLists = {}
for _, pairName in ipairs(pairNames) do
midLists[pairName] = NativePack.collectMidsForPair(grids, borders, pairName, scriptMids)
end
local AnimPack = require("src.import.gba.tileset_anim_pack")
AnimPack.writeExtract(rom2, cache, Extract.CACHE_ROOT, bundles, midLists, version)
local OwExtract = require("src.import.gba.ow_extract")
OwExtract.writeExtract(rom2, cache, Extract.CACHE_ROOT, version)
local EncExtract = require("src.import.gba.encounters_extract")
EncExtract.writeExtract(rom2, cache, Extract.CACHE_ROOT, version)
local FxExtract = require("src.import.gba.field_effect_extract")
FxExtract.writeExtract(rom2, cache, Extract.CACHE_ROOT, version)
do
local MartsExtract = require("src.import.gba.marts_extract")
local okM, detailM = pcall(MartsExtract.run, rom2, cache, {
cacheRoot = Extract.CACHE_ROOT,
})
if okM and detailM then
print(string.format("[marts] %d lists → %s",
detailM.listCount or 0, tostring(detailM.path)))
if not (opts and opts.skipScriptsAndOw) then
local OwExtract = require("src.import.gba.ow_extract")
OwExtract.writeExtract(rom2, cache, Extract.CACHE_ROOT, version)
local EncExtract = require("src.import.gba.encounters_extract")
EncExtract.writeExtract(rom2, cache, Extract.CACHE_ROOT, version)
local FxExtract = require("src.import.gba.field_effect_extract")
FxExtract.writeExtract(rom2, cache, Extract.CACHE_ROOT, version)
do
local MartsExtract = require("src.import.gba.marts_extract")
local okM, detailM = pcall(MartsExtract.run, rom2, cache, {
cacheRoot = Extract.CACHE_ROOT,
})
if okM and detailM then
print(string.format("[marts] %d lists → %s",
detailM.listCount or 0, tostring(detailM.path)))
end
end
for _, name in ipairs({ "online_ui_extract", "berry_crush_extract", "dodrio_extract", "pokemon_jump_extract" }) do
require("src.import.gba." .. name).run(rom2, cache, { cacheRoot = Extract.CACHE_ROOT })
end
end
for _, name in ipairs({ "online_ui_extract", "berry_crush_extract", "dodrio_extract", "pokemon_jump_extract" }) do
require("src.import.gba." .. name).run(rom2, cache, { cacheRoot = Extract.CACHE_ROOT })
end
rom2:clearCache()
end
@@ -542,34 +546,37 @@ function Extract.run(imports, cache, progressCb)
cl[#cl + 1] = "}\n"
cache:write(Extract.CACHE_ROOT .. "/connections.lua", table.concat(cl))
-- game3 scripts/events/text/movements from ROM MapEvents + BFS
local ExtractScripts = require("src.import.gba.extract_scripts")
rom = assert(Rom.open(imports, importId))
local scriptBundle = ExtractScripts.writeBundleFromRom(
rom, cache, Extract.CACHE_ROOT, version, extractedScripts)
local scriptBundle = nil
if not (opts and opts.skipScriptsAndOw) then
-- game3 scripts/events/text/movements from ROM MapEvents + BFS
local ExtractScripts = require("src.import.gba.extract_scripts")
rom = assert(Rom.open(imports, importId))
scriptBundle = ExtractScripts.writeBundleFromRom(
rom, cache, Extract.CACHE_ROOT, version, extractedScripts)
-- Extract full trainer parties, AI flags, dialogs, and sprites
do
local TrainerExtract = require("src.import.gba.trainer_extract")
TrainerExtract.run(rom, cache, {
cacheRoot = Extract.CACHE_ROOT,
scripts = scriptBundle and scriptBundle.scripts,
text = scriptBundle and scriptBundle.text,
})
end
-- Normalized map_tree mirror
do
local MapTreeExtract = require("src.import.gba.map_tree_extract")
local okTree, treeDetail = MapTreeExtract.run(rom, cache, {
version = version,
root = Extract.CACHE_ROOT .. "/map_tree",
})
if not okTree then
print("[extract] map_tree warn: " .. tostring(treeDetail))
-- Extract full trainer parties, AI flags, dialogs, and sprites
do
local TrainerExtract = require("src.import.gba.trainer_extract")
TrainerExtract.run(rom, cache, {
cacheRoot = Extract.CACHE_ROOT,
scripts = scriptBundle and scriptBundle.scripts,
text = scriptBundle and scriptBundle.text,
})
end
-- Normalized map_tree mirror
do
local MapTreeExtract = require("src.import.gba.map_tree_extract")
local okTree, treeDetail = MapTreeExtract.run(rom, cache, {
version = version,
root = Extract.CACHE_ROOT .. "/map_tree",
})
if not okTree then
print("[extract] map_tree warn: " .. tostring(treeDetail))
end
end
rom:clearCache()
end
rom:clearCache()
progress(progressCb, 7, "done", 1, 1)
bundles = nil
@@ -1736,63 +1743,6 @@ local function _dormant_quantize_run(imports, cache, progressCb)
EncExtract.writeExtract(rom2, cache, Extract.CACHE_ROOT, version)
local FxExtract = require("src.import.gba.field_effect_extract")
FxExtract.writeExtract(rom2, cache, Extract.CACHE_ROOT, version)
do
local MartsExtract = require("src.import.gba.marts_extract")
local okM, detailM = pcall(MartsExtract.run, rom2, cache, {
cacheRoot = Extract.CACHE_ROOT,
})
if okM and detailM then
print(string.format("[marts] %d lists → %s",
detailM.listCount or 0, tostring(detailM.path)))
end
end
do
local BagChromeExtract = require("src.import.gba.bag_chrome_extract")
local okB, detailB = pcall(BagChromeExtract.run, rom2, cache, {
cacheRoot = Extract.CACHE_ROOT,
})
if okB and detailB then
print(string.format("[bag_chrome] icons=%d → %s",
detailB.iconsBaked or 0, tostring(detailB.root)))
elseif not okB then
print("[bag_chrome] warn: " .. tostring(detailB))
end
end
do
local AnimExtract = require("src.import.gba.battle_anim_extract")
local animCache = {
write = function(_, rel, bytes)
local path = rel
if not path:match("^data/") then path = Extract.CACHE_ROOT .. "/" .. path end
return cache:write(path, bytes)
end,
exists = function(_, rel) return (cache.exists and cache:exists(rel)) or false end,
read = function(_, rel) return (cache.read and cache:read(rel)) or nil end,
}
pcall(AnimExtract.run, rom2, animCache, { cacheRoot = Extract.CACHE_ROOT, force = true })
end
do
local BattleAiExtract = require("src.import.gba.battle_ai_extract")
BattleAiExtract.run(rom2, cache, { cacheRoot = Extract.CACHE_ROOT })
end
do
local TextChromeExtract = require("src.import.gba.text_chrome_extract")
local okTc, errTc = pcall(TextChromeExtract.run, rom2, cache, { cacheRoot = Extract.CACHE_ROOT })
if not okTc then print("[text_chrome] warn: " .. tostring(errTc)) end
local MapSectionsExtract = require("src.import.gba.map_sections_extract")
local okMs, errMs = pcall(MapSectionsExtract.run, rom2, cache, { cacheRoot = Extract.CACHE_ROOT })
if not okMs then print("[map_sections] warn: " .. tostring(errMs)) end
require("src.import.gba.easy_chat_extract").run(rom2, cache, { cacheRoot = Extract.CACHE_ROOT })
local MapPreviewExtract = require("src.import.gba.map_preview_extract")
local okMp, errMp = pcall(MapPreviewExtract.run, rom2, cache, { cacheRoot = Extract.CACHE_ROOT })
if not okMp then print("[map_preview] warn: " .. tostring(errMp)) end
local RegionMapExtract = require("src.import.gba.region_map_extract")
local okRm, errRm = pcall(RegionMapExtract.run, rom2, cache, { cacheRoot = Extract.CACHE_ROOT })
if not okRm then print("[region_map] warn: " .. tostring(errRm)) end
local HealLocationsExtract = require("src.import.gba.heal_locations_extract")
local okHl, errHl = pcall(HealLocationsExtract.run, rom2, cache, { cacheRoot = Extract.CACHE_ROOT })
if not okHl then print("[heal_locations] warn: " .. tostring(errHl)) end
end
rom2:clearCache()
end
@@ -1932,6 +1882,69 @@ local function _dormant_quantize_run(imports, cache, progressCb)
}
end
function Extract.runScriptsAndOw(imports, cache, progressCb)
local importId, info = Extract.findImport(imports)
if not importId then
return false, "no FireRed/LeafGreen optional import installed"
end
local version, verr = Versions.lookup(info.md5)
if not version then return false, verr end
local rom = assert(Rom.open(imports, importId))
if progressCb then progressCb(1, 4, "help_quest_log", 0, 1) end
require("src.import.gba.help_extract").writeExtract(rom, cache)
require("src.import.gba.quest_log_extract").writeExtract(rom, cache)
require("src.import.gba.object_interactions_extract").writeExtract(rom, cache, Extract.CACHE_ROOT, version)
if progressCb then progressCb(2, 4, "ow_sprites", 0, 1) end
local OwExtract = require("src.import.gba.ow_extract")
OwExtract.writeExtract(rom, cache, Extract.CACHE_ROOT, version)
local EncExtract = require("src.import.gba.encounters_extract")
EncExtract.writeExtract(rom, cache, Extract.CACHE_ROOT, version)
local FxExtract = require("src.import.gba.field_effect_extract")
FxExtract.writeExtract(rom, cache, Extract.CACHE_ROOT, version)
if progressCb then progressCb(3, 4, "scripts_events", 0, 1) end
local ExtractScripts = require("src.import.gba.extract_scripts")
local scriptBundle = ExtractScripts.writeBundleFromRom(rom, cache, Extract.CACHE_ROOT, version)
if progressCb then progressCb(4, 4, "trainers_map_tree", 0, 1) end
local TrainerExtract = require("src.import.gba.trainer_extract")
TrainerExtract.run(rom, cache, {
cacheRoot = Extract.CACHE_ROOT,
scripts = scriptBundle and scriptBundle.scripts,
text = scriptBundle and scriptBundle.text,
})
local MapTreeExtract = require("src.import.gba.map_tree_extract")
local okTree, treeDetail = MapTreeExtract.run(rom, cache, {
version = version,
root = Extract.CACHE_ROOT .. "/map_tree",
})
if not okTree then
print("[extract] map_tree warn: " .. tostring(treeDetail))
end
do
local MartsExtract = require("src.import.gba.marts_extract")
local okM, detailM = pcall(MartsExtract.run, rom, cache, {
cacheRoot = Extract.CACHE_ROOT,
})
if okM and detailM then
print(string.format("[marts] %d lists → %s",
detailM.listCount or 0, tostring(detailM.path)))
end
end
for _, name in ipairs({ "online_ui_extract", "berry_crush_extract", "dodrio_extract", "pokemon_jump_extract" }) do
require("src.import.gba." .. name).run(rom, cache, { cacheRoot = Extract.CACHE_ROOT })
end
rom:clearCache()
return true
end
--- Rebuild native blobs only (demake cache already valid). Seconds, not minutes.
-- Uses full gMapGroups census → CacheFS (same pair set as Extract.run).
function Extract.runNativeOnly(imports, cache, progressCb)
@@ -2039,47 +2052,6 @@ function Extract.runNativeOnly(imports, cache, progressCb)
EncExtract.writeExtract(rom2, cache, Extract.CACHE_ROOT, version)
local FxExtract = require("src.import.gba.field_effect_extract")
FxExtract.writeExtract(rom2, cache, Extract.CACHE_ROOT, version)
do
local MartsExtract = require("src.import.gba.marts_extract")
local okM, detailM = pcall(MartsExtract.run, rom2, cache, {
cacheRoot = Extract.CACHE_ROOT,
})
if okM and detailM then
print(string.format("[marts] %d lists → %s",
detailM.listCount or 0, tostring(detailM.path)))
end
end
do
local BagChromeExtract = require("src.import.gba.bag_chrome_extract")
local okB, detailB = pcall(BagChromeExtract.run, rom2, cache, {
cacheRoot = Extract.CACHE_ROOT,
})
if okB and detailB then
print(string.format("[bag_chrome] icons=%d → %s",
detailB.iconsBaked or 0, tostring(detailB.root)))
elseif not okB then
print("[bag_chrome] warn: " .. tostring(detailB))
end
end
do
local AnimExtract = require("src.import.gba.battle_anim_extract")
local animCache = {
write = function(_, rel, bytes)
local path = rel
if not path:match("^data/") then
path = Extract.CACHE_ROOT .. "/" .. path
end
return cache:write(path, bytes)
end,
exists = function(_, rel) return (cache.exists and cache:exists(rel)) or false end,
read = function(_, rel) return (cache.read and cache:read(rel)) or nil end,
}
pcall(AnimExtract.run, rom2, animCache, { cacheRoot = Extract.CACHE_ROOT, force = true })
end
do
local BattleAiExtract = require("src.import.gba.battle_ai_extract")
BattleAiExtract.run(rom2, cache, { cacheRoot = Extract.CACHE_ROOT })
end
rom2:clearCache()
end
bump_meta_version(cache)
+10 -2
View File
@@ -44,11 +44,16 @@ local ok, err = pcall(function()
end
end, sha1)
local t0 = (love and love.timer and love.timer.getTime()) or os.clock()
local res, detail
if task_name == "gba" then
res, detail = ext:runGbaExtract(sha1)
res, detail = ext:runGbaExtract(sha1, true)
elseif task_name == "scripts_ow" then
res, detail = ext:runScriptsAndOwExtract(sha1)
elseif task_name == "pokemon" then
res, detail = ext:runPokemonExtract(sha1)
res, detail = ext:runPokemonExtract(sha1, 201, 411)
elseif task_name == "pokemon_gfx" then
res, detail = ext:runPokemonGfxExtract(sha1, 0, 200)
elseif task_name == "aux" then
res, detail = ext:runAuxExtracts(sha1)
elseif task_name == "intro_audio" then
@@ -57,6 +62,9 @@ local ok, err = pcall(function()
error("unknown extract task: " .. tostring(task_name))
end
local dt = ((love and love.timer and love.timer.getTime()) or os.clock()) - t0
print(string.format("[worker %s] completed in %.3fs (res=%s)", task_name, dt, tostring(res)))
assert(res ~= false, detail or (task_name .. " extraction failed"))
collectgarbage("collect")
return res
+71
View File
@@ -168,6 +168,77 @@ function Lz77.decompress(get, offset)
return out, src - offset
end
--- Decompress GBA LZ77 directly into a binary Lua string (zero-copy when FFI is available).
function Lz77.decompressString(get, offset)
offset = offset or 0
local get_byte
if type(get) == "function" then
get_byte = get
elseif type(get) == "string" then
get_byte = function(i) return string.byte(get, i + 1) or 0 end
elseif type(get) == "table" and get.get then
get_byte = function(i) return get:get(i) or 0 end
elseif type(get) == "table" then
get_byte = function(i) return get[i + 1] or 0 end
else
error("LZ77: invalid byte source")
end
local typ = get_byte(offset)
if typ ~= 0x10 then
error(("LZ77: expected type 0x10 at 0x%X, got 0x%02X"):format(offset, typ or 0))
end
local size = get_byte(offset + 1) + get_byte(offset + 2) * 256 + get_byte(offset + 3) * 65536
if size <= 0 or size > 8 * 1024 * 1024 then
error(("LZ77: unreasonable size %d"):format(size))
end
local src = offset + 4
local buf = get_ffi_buffer(size)
local band = bit and bit.band
local rshift = bit and bit.rshift
local lshift = bit and bit.lshift
local bor = bit and bit.bor
if buf and band and rshift and bor and lshift and ffi then
local produced = 0
while produced < size do
local flags = get_byte(src)
src = src + 1
for bi = 7, 0, -1 do
if produced >= size then break end
local is_ref = band(rshift(flags, bi), 1) == 1
if is_ref then
local b1 = get_byte(src)
local b2 = get_byte(src + 1)
src = src + 2
local length = rshift(b1, 4) + 3
local disp = bor(lshift(band(b1, 0x0F), 8), b2)
for _ = 1, length do
if produced >= size then break end
local read_pos = produced - 1 - disp
local v = 0
if read_pos >= 0 then
v = buf[read_pos]
end
buf[produced] = v
produced = produced + 1
end
else
buf[produced] = get_byte(src) or 0
src = src + 1
produced = produced + 1
end
end
end
return ffi.string(buf, size), src - offset
end
local out_arr, consumed = Lz77.decompress(get, offset)
return Lz77.toString(out_arr), consumed
end
--- Decompress from a 1-based byte array / string-like source.
function Lz77.decompressFromBytes(bytes, offset0)
local function get(i)
+3
View File
@@ -30,6 +30,9 @@ end
local function rom_blob(rom, ptr, nbytes)
local off = rom:ptrOffset(ptr)
if not off or not nbytes or nbytes < 1 then return nil end
if rom.readString then
return rom:readString(off, nbytes)
end
local bytes = rom:readBytes(off, nbytes)
local s = {}
for i = 1, #bytes do
+87 -46
View File
@@ -123,13 +123,24 @@ local function decode_4bpp(bytes, w, h)
local ti = 0
local band = bit and bit.band
local rshift = bit and bit.rshift
local isStr = type(bytes) == "string"
local bytes_ptr = (isStr and ffi) and ffi.cast("const uint8_t*", bytes) or nil
local bytes_len = isStr and #bytes or (bytes and #bytes or 0)
for ty = 0, tilesH - 1 do
for tx = 0, tilesW - 1 do
local tileOff = ti * 32 -- 32 bytes / 4bpp tile
for row = 0, 7 do
for bx = 0, 3 do
local bi = tileOff + row * 4 + bx + 1
local byte = bytes[bi] or 0
local bi = tileOff + row * 4 + bx
local byte = 0
if bytes_ptr then
if bi < bytes_len then byte = bytes_ptr[bi] end
elseif isStr then
byte = string.byte(bytes, bi + 1) or 0
else
byte = bytes[bi + 1] or 0
end
local p0, p1
if band and rshift then
p0 = band(byte, 0x0F)
@@ -212,9 +223,26 @@ local function decode_pic_sheet(tiles, palBytes, frame, bank)
local palBase = (tonumber(bank) or 0) * 32
local tileBase = (tonumber(frame) or 0) * 2048
local pal = {}
local isStrPal = type(palBytes) == "string"
local isStrTiles = type(tiles) == "string"
local pal_ptr = (isStrPal and ffi) and ffi.cast("const uint8_t*", palBytes) or nil
local tiles_ptr = (isStrTiles and ffi) and ffi.cast("const uint8_t*", tiles) or nil
local pal_len = isStrPal and #palBytes or (palBytes and #palBytes or 0)
local tiles_len = isStrTiles and #tiles or (tiles and #tiles or 0)
for c = 0, 15 do
local lo = palBytes[palBase + c * 2 + 1] or 0
local hi = palBytes[palBase + c * 2 + 2] or 0
local lo, hi
if pal_ptr then
local idx = palBase + c * 2
lo = (idx < pal_len) and pal_ptr[idx] or 0
hi = (idx + 1 < pal_len) and pal_ptr[idx + 1] or 0
elseif isStrPal then
lo = string.byte(palBytes, palBase + c * 2 + 1) or 0
hi = string.byte(palBytes, palBase + c * 2 + 2) or 0
else
lo = palBytes[palBase + c * 2 + 1] or 0
hi = palBytes[palBase + c * 2 + 2] or 0
end
pal[c] = lo + hi * 256
end
local w, h = 64, 64
@@ -236,8 +264,15 @@ local function decode_pic_sheet(tiles, palBytes, frame, bank)
local tileOff = ti * 32
for row = 0, 7 do
for bx = 0, 3 do
local bi = tileBase + tileOff + row * 4 + bx + 1
local byte = tiles[bi] or 0
local bi = tileBase + tileOff + row * 4 + bx
local byte = 0
if tiles_ptr then
if bi < tiles_len then byte = tiles_ptr[bi] end
elseif isStrTiles then
byte = string.byte(tiles, bi + 1) or 0
else
byte = tiles[bi + 1] or 0
end
local p0 = band(byte, 0x0F)
local p1 = rshift(byte, 4)
local x0 = tx * 8 + bx * 2
@@ -285,8 +320,8 @@ local function decode_pic_sheet(tiles, palBytes, frame, bank)
local tileOff = ti * 32
for row = 0, 7 do
for bx = 0, 3 do
local bi = tileBase + tileOff + row * 4 + bx + 1
local byte = tiles[bi] or 0
local bi = tileBase + tileOff + row * 4 + bx
local byte = isStrTiles and (string.byte(tiles, bi + 1) or 0) or (tiles[bi + 1] or 0)
local p0 = byte % 16
local p1 = math.floor(byte / 16) % 16
local x0 = tx * 8 + bx * 2
@@ -682,6 +717,9 @@ function PokemonExtract.run(rom, cache, opts)
local cacheRoot = opts.cacheRoot or default_cache_root()
local root = cacheRoot .. "/" .. PokemonExtract.CACHE_SUB
local num = opts.numSpecies or Versions.NUM_SPECIES
local spMin = opts.spMin or 0
local spMax = opts.spMax or (num - 1)
local onlySpeciesGfx = opts.onlySpeciesGfx == true
local progress = opts.progress
local names = {}
@@ -705,8 +743,10 @@ function PokemonExtract.run(rom, cache, opts)
end
local function lz(off)
local ok, out = pcall(Lz77.decompress, function(i) return rom:get(i) end, off)
if ok then return out end
local ok, out = pcall(Lz77.decompressString, rom, off)
if ok and out then return out end
local ok2, out2 = pcall(Lz77.decompress, function(i) return rom:get(i) end, off)
if ok2 then return out2 end
return nil
end
@@ -731,6 +771,7 @@ function PokemonExtract.run(rom, cache, opts)
-- src/pokemon.c:1350, :5339
if sp == SPECIES_SPINDA and kind == "front" then
local function raw(t, n)
if type(t) == "string" then return t:sub(1, n) end
local out = {}
for i = 1, n do out[i] = string.char(t[i] or 0) end
return table.concat(out)
@@ -757,10 +798,34 @@ function PokemonExtract.run(rom, cache, opts)
end
end
for sp = 0, num - 1 do
if progress and sp % 40 == 0 then
progress("pokemon", sp, num)
local totalGfxCount = spMax - spMin + 1
for sp = spMin, spMax do
if progress and (sp - spMin) % 40 == 0 then
progress("pokemon", sp - spMin, totalGfxCount)
end
put(cache, root .. "/icons/" .. sp .. ".rgba", icon_rgba(rom, sp, pals))
picsWritten.icons = picsWritten.icons + 1
write_pics(sp)
end
if not onlySpeciesGfx and spMax >= num - 1 then
-- include/constants/species.h:425
for sp = Versions.SPECIES_UNOWN_B, Versions.SPECIES_UNOWN_QMARK do
put(cache, root .. "/icons/" .. sp .. ".rgba", icon_rgba(rom, sp, pals))
write_pics(sp)
end
end
if onlySpeciesGfx then
return {
root = root,
numSpecies = totalGfxCount,
picsWritten = picsWritten,
}
end
-- Full metadata pass for all species
for sp = 0, num - 1 do
names[sp] = decode_name(rom, nameBase + sp * Versions.SPECIES_NAME_LENGTH)
local ioff = infoBase + sp * Versions.SPECIES_INFO_SIZE
stats[sp] = {
@@ -783,7 +848,6 @@ function PokemonExtract.run(rom, cache, opts)
end
types[sp] = { rom:get(ioff + 6), rom:get(ioff + 7) }
abilities[sp] = { rom:get(ioff + 0x16), rom:get(ioff + 0x17) }
-- pokefirered/include/pokemon.h:219
local evLo, evHi = rom:get(ioff + 0x0A), rom:get(ioff + 0x0B)
meta[sp] = {
catchRate = rom:get(ioff + 0x08),
@@ -802,22 +866,10 @@ function PokemonExtract.run(rom, cache, opts)
growthRate = rom:get(ioff + 0x13),
eggGroup1 = rom:get(ioff + 0x14),
eggGroup2 = rom:get(ioff + 0x15),
-- pokefirered/include/pokemon.h:233
safariZoneFleeRate = rom:get(ioff + 0x18),
linkStats = linkStats,
}
-- Table omits SPECIES_NONE; SpeciesToNationalPokedexNum uses [species - 1].
toNat[sp] = (sp >= 1) and rom:u16(natBase + (sp - 1) * 2) or 0
put(cache, root .. "/icons/" .. sp .. ".rgba", icon_rgba(rom, sp, pals))
picsWritten.icons = picsWritten.icons + 1
write_pics(sp)
end
-- include/constants/species.h:425
for sp = Versions.SPECIES_UNOWN_B, Versions.SPECIES_UNOWN_QMARK do
put(cache, root .. "/icons/" .. sp .. ".rgba", icon_rgba(rom, sp, pals))
write_pics(sp)
end
if #picsMissing > 0 then
@@ -826,17 +878,18 @@ function PokemonExtract.run(rom, cache, opts)
error(("pokemon_extract: %d species sprites with valid ROM pointers produced no file (%s%s)")
:format(#picsMissing, table.concat(shown, ", "), #picsMissing > #shown and ", ..." or ""))
end
if picsWritten.icons < num or picsWritten.front < 1 or picsWritten.back < 1 then
local expectedIcons = (spMin == 0 and spMax >= num - 1) and num or (spMax - spMin + 1)
if picsWritten.icons < expectedIcons or picsWritten.front < 1 or picsWritten.back < 1 then
error(("pokemon_extract: sprite pass wrote %d icons, %d front, %d back for %d species")
:format(picsWritten.icons, picsWritten.front, picsWritten.back, num))
:format(picsWritten.icons, picsWritten.front, picsWritten.back, expectedIcons))
end
-- pokefirered/src/battle_gfx_sfx_util.c:422
local ghostPic = Versions.GHOST_FRONT_PIC
local ghostPal = Versions.GHOST_PALETTE
if ghostPic and ghostPal then
local okT, tiles = pcall(Lz77.decompress, function(i) return rom:get(i) end, ghostPic)
local okP, palBytes = pcall(Lz77.decompress, function(i) return rom:get(i) end, ghostPal)
local okT, tiles = pcall(Lz77.decompressString, rom, ghostPic)
local okP, palBytes = pcall(Lz77.decompressString, rom, ghostPal)
if okT and okP and tiles and palBytes then
local rgba = decode_pic_sheet(tiles, palBytes)
if rgba then cache:write(root .. "/front/ghost.rgba", rgba) end
@@ -956,8 +1009,8 @@ function PokemonExtract.run(rom, cache, opts)
local moveDescs = {}
local moveDescBase = Versions.MOVE_DESCRIPTIONS or 0x4886E8
local moveCount = (Versions.MOVES_COUNT or 355) - 1
for i = 0, moveCount - 1 do
local moveDescCount = (Versions.MOVES_COUNT or 355) - 1
for i = 0, moveDescCount - 1 do
local ptr = rom:u32(moveDescBase + i * 4)
local off = gba_off(ptr)
local name = moveNames[i + 1] or ("MOVE_" .. (i + 1))
@@ -1056,12 +1109,12 @@ function PokemonExtract.run(rom, cache, opts)
if progress then progress("bag_chrome", 0, 1) end
local BagChromeExtract = require("src.import.gba.bag_chrome_extract")
local bagChrome = BagChromeExtract.run(rom, cache, { cacheRoot = cacheRoot })
BagChromeExtract.run(rom, cache, { cacheRoot = cacheRoot })
if progress then progress("bag_chrome", 1, 1) end
if progress then progress("shop_chrome", 0, 1) end
local ShopChromeExtract = require("src.import.gba.shop_chrome_extract")
local shopChrome = ShopChromeExtract.run(rom, cache, { cacheRoot = cacheRoot })
ShopChromeExtract.run(rom, cache, { cacheRoot = cacheRoot })
if progress then progress("shop_chrome", 1, 1) end
local TextChromeExtract = require("src.import.gba.text_chrome_extract")
@@ -1091,16 +1144,6 @@ function PokemonExtract.run(rom, cache, opts)
require("src.import.gba.easy_chat_extract").run(rom, cache, { cacheRoot = cacheRoot })
if progress then progress("trainers", 0, 1) end
local TrainerExtract = require("src.import.gba.trainer_extract")
local trainers = TrainerExtract.run(rom, cache, { cacheRoot = cacheRoot })
if progress then progress("trainers", 1, 1) end
if progress then progress("battle_ai", 0, 1) end
local BattleAiExtract = require("src.import.gba.battle_ai_extract")
local battleAi = BattleAiExtract.run(rom, cache, { cacheRoot = cacheRoot or default_cache_root() })
if progress then progress("battle_ai", 1, 1) end
return {
root = root,
numSpecies = num,
@@ -1119,8 +1162,6 @@ function PokemonExtract.run(rom, cache, opts)
battleChrome = battleChrome,
battleTransition = battleTransition,
summaryChrome = summaryChrome,
trainers = trainers,
battleAi = battleAi,
}
end
+130 -49
View File
@@ -1,97 +1,178 @@
-- Bound ROM reads via mod.imports (8 MiB max per call) into numeric byte tables.
-- Zero-copy high-performance ROM accessor with LuaJIT FFI pointer anchoring
-- and ARM-safe byte arithmetic (no unaligned multi-byte typed pointer casts).
local RevisionView = require("src.import.gba.revision_view")
local ffi
do
local ok, mod = pcall(require, "ffi")
if ok and mod and mod.cast and mod.string then
ffi = mod
end
end
local bit = rawget(_G, "bit") or rawget(_G, "bit32")
if not bit then
local ok, mod = pcall(require, "bit")
if ok and mod then bit = mod end
end
local Rom = {}
Rom.__index = Rom
local CHUNK = 4 * 1024 * 1024 -- under ImportAccess.MAX_READ_BYTES (8 MiB)
local CHUNK_READ_MAX = 8 * 1024 * 1024 -- ImportAccess.MAX_READ_BYTES limit
function Rom.open(imports, importId)
local info, err = imports:info(importId)
if not info then return nil, err end
require("src.import.gba.versions").select(info.md5)
local self = setmetatable({
imports = imports,
id = importId,
size = info.size,
md5 = info.md5,
_cache = {}, -- optional page cache: pageIndex → 1-based bytes
_info = info,
_raw = nil,
_cdata = nil,
}, Rom)
self._view = RevisionView.forImports(imports, importId, info)
self:ensureBuffer()
return self
end
local function page_index(offset)
return math.floor(offset / CHUNK)
end
function Rom:ensureBuffer()
if self._raw and (#self._raw == self.size) then
return self._raw
end
function Rom:ensurePage(page)
if self._cache[page] then return self._cache[page] end
local offset = page * CHUNK
local length = math.min(CHUNK, self.size - offset)
if length <= 0 then return nil end
local data, err
if self._view then
data = self._view:sub(offset + 1, offset + length)
local info = self._info or (self.imports and self.imports:info(self.id))
local view = RevisionView.forImports(self.imports, self.id, info)
if type(view) == "string" and #view == self.size then
self._raw = view
else
data, err = self.imports:read(self.id, offset, length)
local chunks = {}
local pos = 0
while pos < self.size do
local len = math.min(CHUNK_READ_MAX, self.size - pos)
local part, rerr
if view then
part = view:sub(pos + 1, pos + len)
else
part, rerr = self.imports:read(self.id, pos, len)
end
if not part then error(rerr or "rom read failed") end
chunks[#chunks + 1] = part
pos = pos + len
end
self._raw = table.concat(chunks)
end
if not data then error(err or "rom read failed") end
local bytes = {}
for i = 1, #data do
bytes[i] = data:byte(i)
-- Anchor FFI pointer directly to self._raw on instance
if ffi and self._raw then
self._cdata = ffi.cast("const uint8_t*", self._raw)
end
-- Drop oldest pages if cache grows large (keep ≤3 pages ≈ 12 MiB)
local n = 0
for _ in pairs(self._cache) do n = n + 1 end
if n >= 3 then
self._cache = {}
end
self._cache[page] = bytes
return bytes
return self._raw
end
--- Byte at 0-based ROM offset.
function Rom:get(offset)
if offset < 0 or offset >= self.size then
error(("ROM OOB 0x%X"):format(offset))
error(("ROM OOB 0x%X (size 0x%X)"):format(offset, self.size))
end
local page = page_index(offset)
local bytes = self:ensurePage(page)
local localIndex = (offset % CHUNK) + 1
return bytes[localIndex]
if not self._raw then self:ensureBuffer() end
if self._cdata then
return self._cdata[offset]
end
return string.byte(self._raw, offset + 1) or 0
end
--- Read little-endian u16 with ARM-safe byte arithmetic.
function Rom:u16(offset)
if offset < 0 or offset + 1 >= self.size then
error(("ROM OOB u16 0x%X"):format(offset))
end
if not self._raw then self:ensureBuffer() end
if self._cdata then
local cd = self._cdata
if bit and bit.lshift then
return cd[offset] + bit.lshift(cd[offset + 1], 8)
end
return cd[offset] + cd[offset + 1] * 256
end
local b0, b1 = string.byte(self._raw, offset + 1, offset + 2)
return (b0 or 0) + (b1 or 0) * 256
end
--- Read little-endian u32 with ARM-safe byte arithmetic.
function Rom:u32(offset)
if offset < 0 or offset + 3 >= self.size then
error(("ROM OOB u32 0x%X"):format(offset))
end
if not self._raw then self:ensureBuffer() end
if self._cdata then
local cd = self._cdata
if bit and bit.lshift then
return cd[offset]
+ bit.lshift(cd[offset + 1], 8)
+ bit.lshift(cd[offset + 2], 16)
+ cd[offset + 3] * 16777216
end
return cd[offset]
+ cd[offset + 1] * 256
+ cd[offset + 2] * 65536
+ cd[offset + 3] * 16777216
end
local b0, b1, b2, b3 = string.byte(self._raw, offset + 1, offset + 4)
return (b0 or 0)
+ (b1 or 0) * 256
+ (b2 or 0) * 65536
+ (b3 or 0) * 16777216
end
--- Fast binary slice read (returns binary Lua string).
function Rom:readString(offset, length)
if offset < 0 or length < 0 or offset + length > self.size then
error(("ROM OOB readString 0x%X + %d"):format(offset, length))
end
if length == 0 then return "" end
if not self._raw then self:ensureBuffer() end
if self._cdata and ffi then
return ffi.string(self._cdata + offset, length)
end
return self._raw:sub(offset + 1, offset + length)
end
--- Read `length` bytes at 0-based offset into a new 1-based array.
function Rom:readBytes(offset, length)
if offset < 0 or length < 0 or offset + length > self.size then
error(("ROM OOB readBytes 0x%X + %d"):format(offset, length))
end
if not self._raw then self:ensureBuffer() end
local out = {}
for i = 0, length - 1 do
out[i + 1] = self:get(offset + i)
if self._cdata then
local cd = self._cdata
for i = 0, length - 1 do
out[i + 1] = cd[offset + i]
end
else
local raw = self._raw
for i = 0, length - 1 do
out[i + 1] = string.byte(raw, offset + i + 1)
end
end
return out
end
--- Read little-endian u16 / u32.
function Rom:u16(offset)
return self:get(offset) + self:get(offset + 1) * 256
end
function Rom:u32(offset)
return self:get(offset)
+ self:get(offset + 1) * 256
+ self:get(offset + 2) * 65536
+ self:get(offset + 3) * 16777216
end
--- GBA pointer (0x08XXXXXX) → file offset, or nil.
function Rom:ptrOffset(gbaPtr)
if gbaPtr < 0x08000000 or gbaPtr >= 0x0A000000 then return nil end
if not gbaPtr or gbaPtr < 0x08000000 or gbaPtr >= 0x0A000000 then return nil end
return gbaPtr - 0x08000000
end
function Rom:clearCache()
self._cache = {}
-- Rom holds contiguous buffer for the lifetime of open ROM handle
end
return Rom
+35 -21
View File
@@ -10,11 +10,6 @@ Tileset.NUM_PRIMARY_METATILES = 640
Tileset.NUM_PALS_IN_PRIMARY = 7 -- BG slots 0..6
Tileset.NUM_PALS_TOTAL = 13 -- BG slots 0..12
local function bytes_to_array(str)
local t = {}
for i = 1, #str do t[i] = str:byte(i) end
return t
end
--- Decode one BGR555 colour → r,g,b in 0..31 (5-bit) and 0..255.
function Tileset.bgr555(c)
@@ -68,18 +63,20 @@ end
--- Decompress 4bpp tiles → array of tileCount entries, each 64 nybbles (8×8).
function Tileset.loadTiles4bpp(rom, lzOffset)
local raw, _consumed = Lz77.decompress(function(i) return rom:get(i) end, lzOffset)
local nbytes = type(raw) == "table" and (raw._len or #raw) or #raw
local ok, raw = pcall(Lz77.decompressString, rom, lzOffset)
if not ok or not raw then
raw = Lz77.decompress(function(i) return rom:get(i) end, lzOffset)
end
local nbytes = type(raw) == "string" and #raw or (type(raw) == "table" and (raw._len or #raw) or 0)
local tileCount = math.floor(nbytes / 32)
local tiles = { count = tileCount, raw = raw }
return tiles
return { count = tileCount, raw = raw }
end
--- Uncompressed 4bpp tile sheet (isCompressed=0 tilesets, e.g. cable club).
function Tileset.loadTiles4bppRaw(rom, offset, nbytes)
nbytes = tonumber(nbytes) or 0
if nbytes < 32 then return { count = 0, raw = {} } end
local raw = rom:readBytes(offset, nbytes)
if nbytes < 32 then return { count = 0, raw = "" } end
local raw = rom:readString(offset, nbytes)
return { count = math.floor(nbytes / 32), raw = raw }
end
@@ -93,8 +90,10 @@ function Tileset.tileIndex(tiles, tid, x, y)
local b
if raw._ffi then
b = raw._ffi[byteIndex]
elseif type(raw) == "string" then
b = string.byte(raw, byteIndex + 1) or 0
else
b = raw[byteIndex + 1]
b = raw[byteIndex + 1] or 0
end
if x % 2 == 0 then
return b % 16
@@ -103,13 +102,13 @@ function Tileset.tileIndex(tiles, tid, x, y)
end
function Tileset.loadMetatiles(rom, offset, nbytes)
local data = rom:readBytes(offset, nbytes)
local data = rom:readString(offset, nbytes)
local count = math.floor(nbytes / 16)
return { data = data, count = count }
end
function Tileset.loadAttributes(rom, offset, nbytes)
local data = rom:readBytes(offset, nbytes)
local data = rom:readString(offset, nbytes)
local count = math.floor(nbytes / 4)
return { data = data, count = count }
end
@@ -125,9 +124,15 @@ function Tileset.attrOf(attrs, mid, primaryCount)
if idx < 0 or idx >= attrs.count then
return 0, 0
end
local off = idx * 4 -- 1-based data
local off = idx * 4
local d = attrs.data
local w = d[off + 1] + d[off + 2] * 256 + d[off + 3] * 65536 + d[off + 4] * 16777216
local w
if type(d) == "string" then
local b1, b2, b3, b4 = string.byte(d, off + 1, off + 4)
w = (b1 or 0) + (b2 or 0) * 256 + (b3 or 0) * 65536 + (b4 or 0) * 16777216
else
w = d[off + 1] + d[off + 2] * 256 + d[off + 3] * 65536 + d[off + 4] * 16777216
end
local behavior = w % 512 -- 0x1FF
-- Collision in map cells is separate; metatile attr also encodes layer type etc.
-- Use bit 0x200 area: pret stores collision in map grid, behavior here.
@@ -143,12 +148,21 @@ function Tileset.metatileEntries(primaryMt, secondaryMt, mid)
data = secondaryMt.data
localMid = mid - Tileset.NUM_PRIMARY_METATILES
end
local off = localMid * 16 -- 1-based: 16 bytes = 8 u16
if off + 16 > #data then return nil end
local off = localMid * 16
local dataLen = type(data) == "string" and #data or (data and (data._len or #data) or 0)
if off + 16 > dataLen then return nil end
local entries = {}
for i = 0, 7 do
local b = off + i * 2
entries[i + 1] = data[b + 1] + data[b + 2] * 256
if type(data) == "string" then
for i = 0, 7 do
local b = off + i * 2
local b1, b2 = string.byte(data, b + 1, b + 2)
entries[i + 1] = (b1 or 0) + (b2 or 0) * 256
end
else
for i = 0, 7 do
local b = off + i * 2
entries[i + 1] = data[b + 1] + data[b + 2] * 256
end
end
return entries
end
+9 -3
View File
@@ -282,10 +282,16 @@ function TmCaseExtract.run(rom, cache, opts)
cache:write(string.format("%s/disc_hm_%d.rgba", root, typeIdx), hmDiscRgba)
end
-- 4. HM Icon
-- 4. HM Icon (16x16 tiled bitmap in ROM, 16x12 visible; FRLG tm_case.c:1491,1589)
local hmGfx = {}
for i = 1, 96 do hmGfx[i] = rom:get(Versions.TM_CASE_HM_GFX + i - 1) end
local hmRgba = bake_hm_icon_rgba(hmGfx, banks1[0] or {})
for i = 1, 128 do hmGfx[i] = rom:get(Versions.TM_CASE_HM_GFX + i - 1) end
-- FRLG sPal3Override on WIN_LIST palette 15: color 6 = RGB(8,8,8), color 7 = RGB(30,16,6)
local hmPal = {
[0] = 0,
[6] = 8 + 8 * 32 + 8 * 1024, -- RGB(8, 8, 8) = 0x2108 (dark grey)
[7] = 30 + 16 * 32 + 6 * 1024, -- RGB(30, 16, 6) = 0x1A1E (orange)
}
local hmRgba = bake_hm_icon_rgba(hmGfx, hmPal)
cache:write(root .. "/hm_icon.rgba", hmRgba)
local manifest = string.format([[
+1 -1
View File
@@ -1609,7 +1609,7 @@ Versions.TM_CASE_MENU_FEMALE_PAL = 0xE84D20 -- gTMCaseMenu_Female_Pal (LZ 4
Versions.TM_CASE_DISC_GFX = 0xE84D90 -- gTMCaseDisc_Gfx (LZ 4bpp, 1024 bytes)
Versions.TM_CASE_DISC_TYPES1_PAL = 0xE84F20 -- gTMCaseDiscTypes1_Pal (LZ 16 banks, 512 bytes)
Versions.TM_CASE_DISC_TYPES2_PAL = 0xE85068 -- gTMCaseDiscTypes2_Pal (LZ 1 bank, 32 bytes)
Versions.TM_CASE_HM_GFX = 0xE99138 -- gTMCaseHM_Gfx (raw 4bpp, 96 bytes)
Versions.TM_CASE_HM_GFX = 0xE99118 -- gTMCaseHM_Gfx (raw 4bpp, 128 bytes)
-- Berry Pouch (LZ-compressed; FireRed USA 1.0).
Versions.BERRY_POUCH_SPRITE_GFX = 0xE8560C -- gBerryPouchSpriteTiles (LZ 4bpp 64x64, 2048 bytes)
@@ -41,7 +41,7 @@ end
-- Mock isReady
RomImporter.isReady = function(v)
return v == "red" or v == "gold" or v == "blue" or v == "yellow"
or v == "silver" or v == "crystal"
or v == "silver" or v == "crystal" or v == "firered" or v == "leafgreen"
end
local ok = RomImporter.syncAndroidShortcuts("gold")
@@ -65,6 +65,16 @@ check(capturedShortcuts[2] == "red" and capturedShortcuts[3] == "blue"
and capturedShortcuts[4] == "yellow",
"and the rest still follow GameVersion.ORDER until the cap")
capturedShortcuts = nil
RomImporter.syncAndroidShortcuts("firered")
check(#capturedShortcuts == 4, "firered shortcut sync caps at 4")
check(capturedShortcuts[1] == "firered", "activeVersion 'firered' is placed first")
capturedShortcuts = nil
RomImporter.syncAndroidShortcuts("leafgreen")
check(#capturedShortcuts == 4, "leafgreen shortcut sync caps at 4")
check(capturedShortcuts[1] == "leafgreen", "activeVersion 'leafgreen' is placed first")
-- Test with subset of ready games (e.g. only Red and Gold)
RomImporter.isReady = function(v)
return v == "red" or v == "gold"
+41
View File
@@ -40,6 +40,47 @@ check(reqSet["data/generated/gba/items/bag/bg.rgba"] == true, "contract has bag
check(reqSet["data/generated/gba/items/shop/manifest.lua"] == true, "contract has shop manifest.lua")
check(reqSet["data/generated/gba/items/shop/bg.rgba"] == true, "contract has shop bg.rgba")
print("[test] 2b. Bag Chrome Pixel Buffer Decoding")
local BagChromeExtract = require("src.import.gba.bag_chrome_extract")
local Rom = require("src.import.gba.rom")
local f = io.open("1636 - Pokemon Fire Red (U)(Squirrels).gba", "rb")
if f then
local content = f:read("*a")
f:close()
local mockImports = {
info = function() return { size = #content, md5 = "dummy" } end,
read = function(self, id, pos, len) return content:sub(pos + 1, pos + len) end,
}
local testRom = Rom.open(mockImports, "firered")
local testStore = {}
local testCache = {
write = function(self, p, d) testStore[p] = d end,
read = function(self, p) return testStore[p] end,
exists = function(self, p) return testStore[p] ~= nil end,
}
BagChromeExtract.run(testRom, testCache, { cacheRoot = "data/generated/gba" })
local bg = testCache:read("data/generated/gba/items/bag/bg.rgba")
local bagMale = testCache:read("data/generated/gba/items/bag/bag_male.rgba")
check(bg and #bg == 240 * 160 * 4, "bg.rgba extracted with correct size")
check(bagMale and #bagMale == 64 * 256 * 4, "bag_male.rgba extracted with correct size")
local bgColors = {}
for i = 1, #bg, 4 do
local k = string.format("%d,%d,%d,%d", bg:byte(i, i + 3))
bgColors[k] = (bgColors[k] or 0) + 1
end
local distinctBg = 0
for _ in pairs(bgColors) do distinctBg = distinctBg + 1 end
check(distinctBg > 10, "bg.rgba has diverse colors (not solid mono-color): " .. tostring(distinctBg))
local bagColors = {}
for i = 1, #bagMale, 4 do
local k = string.format("%d,%d,%d,%d", bagMale:byte(i, i + 3))
bagColors[k] = (bagColors[k] or 0) + 1
end
local distinctBag = 0
for _ in pairs(bagColors) do distinctBag = distinctBag + 1 end
check(distinctBag > 8, "bag_male.rgba has non-zero sprites: " .. tostring(distinctBag))
end
print("[test] 3. BagMenu Lifecycle & Pockets")
local Bag = require("src.core.game3.bag")
local BagMenu = require("src.ui.game3.bag_menu")
@@ -26,7 +26,7 @@ check(Versions.TM_CASE_MENU_FEMALE_PAL == 0xE84D20, "Versions.TM_CASE_MENU_FEMAL
check(Versions.TM_CASE_DISC_GFX == 0xE84D90, "Versions.TM_CASE_DISC_GFX is 0xE84D90")
check(Versions.TM_CASE_DISC_TYPES1_PAL == 0xE84F20, "Versions.TM_CASE_DISC_TYPES1_PAL is 0xE84F20")
check(Versions.TM_CASE_DISC_TYPES2_PAL == 0xE85068, "Versions.TM_CASE_DISC_TYPES2_PAL is 0xE85068")
check(Versions.TM_CASE_HM_GFX == 0xE99138, "Versions.TM_CASE_HM_GFX is 0xE99138")
check(Versions.TM_CASE_HM_GFX == 0xE99118, "Versions.TM_CASE_HM_GFX is 0xE99118")
check(Versions.BERRY_POUCH_SPRITE_GFX == 0xE8560C, "Versions.BERRY_POUCH_SPRITE_GFX is 0xE8560C")
check(Versions.BERRY_POUCH_BG_GFX == 0xE859D0, "Versions.BERRY_POUCH_BG_GFX is 0xE859D0")
@@ -37,14 +37,23 @@ check(Versions.BERRY_POUCH_BG_TILEMAP == 0xE85C44, "Versions.BERRY_POUCH_BG_TILE
print("\n=== 2. Extraction from FireRed ROM ===")
local romPath = "1636 - Pokemon Fire Red (U)(Squirrels).gba"
local f = io.open(romPath, "rb")
local candidatePaths = {
"1636 - Pokemon Fire Red (U)(Squirrels).gba",
"firered_dump.gba",
"Pokemon - Fire Red Version (U) (V1.1).gba",
}
local f, romPath
for _, p in ipairs(candidatePaths) do
f = io.open(p, "rb")
if f then romPath = p; break end
end
if not f then
print("[SKIP] ROM not found at " .. romPath)
print("[SKIP] ROM not found in candidate paths")
os.exit(0)
end
local romData = f:read("*all")
f:close()
print("Loaded ROM: " .. romPath)
local rom = {
get = function(self, i)
+9
View File
@@ -47,10 +47,19 @@ local function test_lz77_bounds_safety()
print("✓ test_lz77_bounds_safety passed")
end
local function test_lz77_decompress_string()
local payload = "ZERO COPY STRING DECOMPRESSION TEST PAYLOAD 1234567890ABCDEF"
local compressed = Lz77.compressStore(payload)
local decompStr, consumed = Lz77.decompressString(function(i) return compressed[i + 1] end, 0)
assert(decompStr == payload, "decompressString should match original payload")
print("✓ test_lz77_decompress_string passed")
end
local function run_all()
test_lz77_basic()
test_lz77_matches()
test_lz77_bounds_safety()
test_lz77_decompress_string()
print("All LZ77 unit tests passed!")
end
+355
View File
@@ -0,0 +1,355 @@
-- Unit tests for Save Editor Gen 3 EV and IV tracking, editing, caps, and rendering.
-- Run with: luajit tests/save_editor_gen3_ev_iv_tests.lua
package.path = package.path .. ";./?.lua;./?/init.lua;./tools/save-editor/?.lua"
.. ";./tools/save-editor/panels/?.lua"
_G.love = require("tests.love_stub")
local MonOps = require("MonOps")
local Ops = require("Ops")
local Gen = require("Gen")
local Pokemon = require("src.core.game3.pokemon")
Pokemon._stats = { [25] = { hp = 35, atk = 55, def = 40, spe = 90, spa = 50, spd = 50 } }
Pokemon._names = { [25] = "PIKACHU" }
Pokemon._byName = { PIKACHU = 25 }
Pokemon._speciesMeta = { [25] = { growthRate = 0, genderRatio = 127 } }
Pokemon._abilities = { [25] = { 9, 0 } }
Pokemon._abilityNames = { [9] = "STATIC" }
local MonEditor = require("panels.MonEditor")
local Kit = require("Kit")
local passed = 0
local failed = 0
local function check(cond, msg)
if cond then
passed = passed + 1
else
failed = failed + 1
print("FAIL: " .. tostring(msg))
end
end
local function checkEq(actual, expected, msg)
if actual == expected then
passed = passed + 1
else
failed = failed + 1
print(("FAIL: %s (got %s, want %s)"):format(tostring(msg), tostring(actual), tostring(expected)))
end
end
print("== Save Editor Gen 3 EV / IV Tests ==")
local function freshMonG3()
return {
species = 25, -- Pikachu
speciesId = 25,
level = 50,
personality = 0,
ivs = { hp = 31, atk = 31, def = 31, spe = 31, spa = 31, spd = 31 },
evs = { hp = 0, atk = 0, def = 0, spe = 0, spa = 0, spd = 0 },
stats = {},
}
end
-- 1. Single-stat EV clamping (0 .. 255)
do
local mon = freshMonG3()
local val = MonOps.setEv({}, mon, "spe", 252, 3)
checkEq(val, 252, "setEv sets 252 Spe EV")
checkEq(mon.evs.spe, 252, "mon.evs.spe updated to 252")
-- Negative clamping
val = MonOps.setEv({}, mon, "spe", -10, 3)
checkEq(val, 0, "setEv clamps negative value to 0")
checkEq(mon.evs.spe, 0, "mon.evs.spe updated to 0")
-- Upper bound 255 clamping
val = MonOps.setEv({}, mon, "atk", 300, 3)
checkEq(val, 255, "setEv clamps value > 255 to 255")
checkEq(mon.evs.atk, 255, "mon.evs.atk updated to 255")
end
-- 2. 510 Total EV Cap Enforcement
do
local mon = freshMonG3()
MonOps.setEv({}, mon, "atk", 255, 3)
MonOps.setEv({}, mon, "spe", 255, 3)
checkEq(mon.evs.atk + mon.evs.spe, 510, "two stats maxed to 510 total")
-- Attempt to add EV to a 3rd stat when 510 total reached
local val = MonOps.setEv({}, mon, "hp", 10, 3)
checkEq(val, 0, "setEv returns 0 when total EV cap 510 is full")
checkEq(mon.evs.hp, 0, "mon.evs.hp remains 0")
-- Partial addition when close to 510
MonOps.setEv({}, mon, "spe", 250, 3) -- total is now 255 + 250 = 505 (5 remaining)
val = MonOps.setEv({}, mon, "hp", 20, 3)
checkEq(val, 5, "setEv truncates to remaining capacity (5)")
checkEq(mon.evs.hp, 5, "mon.evs.hp set to 5")
checkEq(mon.evs.atk + mon.evs.spe + mon.evs.hp, 510, "total equals exactly 510")
end
-- 3. Clear EVs
do
local mon = freshMonG3()
MonOps.setEv({}, mon, "atk", 252, 3)
MonOps.setEv({}, mon, "spe", 252, 3)
MonOps.setEv({}, mon, "hp", 4, 3)
MonOps.clearEvs({}, mon, 3)
for _, k in ipairs({ "hp", "atk", "def", "spe", "spa", "spd" }) do
checkEq(mon.evs[k], 0, "clearEvs zeroes " .. k)
end
end
-- 4. IV Setting and Clamping (0 .. 31)
do
local mon = freshMonG3()
local val = MonOps.setIv({}, mon, "spe", 31, 3)
checkEq(val, 31, "setIv sets 31 Spe IV")
checkEq(mon.ivs.spe, 31, "mon.ivs.spe updated to 31")
val = MonOps.setIv({}, mon, "spe", -5, 3)
checkEq(val, 0, "setIv clamps negative IV to 0")
checkEq(mon.ivs.spe, 0, "mon.ivs.spe updated to 0")
val = MonOps.setIv({}, mon, "atk", 50, 3)
checkEq(val, 31, "setIv clamps IV > 31 to 31")
checkEq(mon.ivs.atk, 31, "mon.ivs.atk updated to 31")
-- Max all IVs
MonOps.setIv({}, mon, "hp", 0, 3)
MonOps.setIv({}, mon, "atk", 0, 3)
MonOps.maxIvs({}, mon, 3)
for _, k in ipairs({ "hp", "atk", "def", "spe", "spa", "spd" }) do
checkEq(mon.ivs[k], 31, "maxIvs sets " .. k .. " to 31")
end
end
-- 5. Ops wrappers
do
local S = {
data = {},
save = { version = "firered", engine = "game3", generation = 3 },
version = "firered",
editingMon = freshMonG3(),
}
local mon = S.editingMon
Ops.setEv(S, mon, "spe", 252)
checkEq(mon.evs.spe, 252, "Ops.setEv sets speed EV")
Ops.clearEvs(S, mon)
checkEq(mon.evs.spe, 0, "Ops.clearEvs clears speed EV")
Ops.setIv(S, mon, "spa", 30)
checkEq(mon.ivs.spa, 30, "Ops.setIv sets spAtk IV")
Ops.maxIvs(S, mon)
checkEq(mon.ivs.spa, 31, "Ops.maxIvs maxes spAtk IV")
end
-- 6. Real-time Stat Recalculation Impact
do
local mon = freshMonG3()
local baseStats = Pokemon.calcStats(25, 50, mon.ivs, mon.evs, 0)
checkEq(baseStats.speed, 110, "Base Speed at Lv50 with 31 IV, 0 EV is 110")
MonOps.setEv({}, mon, "spe", 252, 3)
local boostedStats = Pokemon.calcStats(25, 50, mon.ivs, mon.evs, 0)
checkEq(boostedStats.speed, 142, "Boosted Speed at Lv50 with 31 IV, 252 EV is 142")
checkEq(boostedStats.speed - baseStats.speed, 32, "252 Spe EVs at Lv50 with 31 IV gives +32 Speed")
-- With 0 IVs
local bareStats = Pokemon.calcStats(25, 50, { spe = 0 }, { spe = 0 }, 0)
local maxEvStats = Pokemon.calcStats(25, 50, { spe = 0 }, { spe = 252 }, 0)
checkEq(maxEvStats.speed - bareStats.speed, 31, "252 Spe EVs at Lv50 with 0 IV gives +31 Speed")
end
-- 7. MonEditor.draw Smoke Tests (Gen 3 wide and narrow)
do
local S = {
data = { moves = {} },
save = { version = "firered", engine = "game3", generation = 3 },
version = "firered",
editingMon = freshMonG3(),
inspectorScroll = 0,
}
-- Wide mode
local okWide, errWide = pcall(function()
MonEditor.draw(S, Kit, 10, 10, 600, 700)
end)
check(okWide, "MonEditor.draw renders Gen 3 wide mode without error: " .. tostring(errWide))
-- Narrow mode
local okNarrow, errNarrow = pcall(function()
MonEditor.draw(S, Kit, 10, 10, 400, 700)
end)
check(okNarrow, "MonEditor.draw renders Gen 3 narrow mode without error: " .. tostring(errNarrow))
end
-- 8. MonEditor.draw Smoke Tests (Gen 1 & Gen 2 non-regression)
do
local S1 = {
data = { moves = {}, pokemon = { PIKACHU = { name = "PIKACHU" } } },
save = { version = "red", generation = 1 },
version = "red",
editingMon = { species = "PIKACHU", level = 50, dvs = { attack = 15, defense = 15, speed = 15, special = 15, hp = 15 } },
inspectorScroll = 0,
}
local okG1, errG1 = pcall(function()
MonEditor.draw(S1, Kit, 10, 10, 500, 700)
end)
check(okG1, "MonEditor.draw renders Gen 1 without error: " .. tostring(errG1))
local S2 = {
data = { moves = {}, pokemon = { PIKACHU = { name = "PIKACHU" } } },
save = { version = "gold", generation = 2 },
version = "gold",
editingMon = { species = "PIKACHU", level = 50, dvs = { attack = 15, defense = 15, speed = 15, special = 15, hp = 15 } },
inspectorScroll = 0,
}
local okG2, errG2 = pcall(function()
MonEditor.draw(S2, Kit, 10, 10, 500, 700)
end)
check(okG2, "MonEditor.draw renders Gen 2 without error: " .. tostring(errG2))
end
-- 9. PP Calculation and PP Ups Formula
do
-- Base 10: 10 + floor(10 * 0.2 * N) = 10, 12, 14, 16
checkEq(MonOps.calcMaxPp(10, 0), 10, "Base 10 PP + 0 Ups = 10")
checkEq(MonOps.calcMaxPp(10, 1), 12, "Base 10 PP + 1 Up = 12")
checkEq(MonOps.calcMaxPp(10, 2), 14, "Base 10 PP + 2 Ups = 14")
checkEq(MonOps.calcMaxPp(10, 3), 16, "Base 10 PP + 3 Ups = 16")
-- Base 5: 5 + floor(5 * 0.2 * N) = 5, 6, 7, 8
checkEq(MonOps.calcMaxPp(5, 0), 5, "Base 5 PP + 0 Ups = 5")
checkEq(MonOps.calcMaxPp(5, 1), 6, "Base 5 PP + 1 Up = 6")
checkEq(MonOps.calcMaxPp(5, 2), 7, "Base 5 PP + 2 Ups = 7")
checkEq(MonOps.calcMaxPp(5, 3), 8, "Base 5 PP + 3 Ups = 8")
-- Base 40: 40 + floor(40 * 0.2 * N) = 40, 48, 56, 64
checkEq(MonOps.calcMaxPp(40, 0), 40, "Base 40 PP + 0 Ups = 40")
checkEq(MonOps.calcMaxPp(40, 1), 48, "Base 40 PP + 1 Up = 48")
checkEq(MonOps.calcMaxPp(40, 2), 56, "Base 40 PP + 2 Ups = 56")
checkEq(MonOps.calcMaxPp(40, 3), 64, "Base 40 PP + 3 Ups = 64")
end
-- 10. Gen 3 PP Ups and PP Editing
do
local mon = freshMonG3()
mon.moves = { 85, 98, 0, 0 } -- Thunderbolt (base 15), Quick Attack (base 30)
mon.pp = { 15, 30, 0, 0 }
mon.maxPp = { 15, 30, 0, 0 }
mon.ppBonusesPacked = 0
local data = {
moves = {
[85] = { name = "THUNDERBOLT", pp = 15 },
[98] = { name = "QUICK ATTACK", pp = 30 },
}
}
-- Check initial PP Ups
checkEq(MonOps.getPpUps(mon, 1), 0, "Slot 1 initial PP Ups is 0")
checkEq(MonOps.getPpUps(mon, 2), 0, "Slot 2 initial PP Ups is 0")
-- Set Slot 1 to 3 PP Ups (15 -> 24 max PP)
MonOps.setPpUps(data, mon, 1, 3, 3)
checkEq(MonOps.getPpUps(mon, 1), 3, "Slot 1 PP Ups is 3")
checkEq(mon.maxPp[1], 24, "Slot 1 max PP updated to 24")
checkEq(mon.ppBonusesPacked, 3, "ppBonusesPacked is 3")
-- Set Slot 2 to 2 PP Ups (30 -> 42 max PP)
MonOps.setPpUps(data, mon, 2, 2, 3)
checkEq(MonOps.getPpUps(mon, 2), 2, "Slot 2 PP Ups is 2")
checkEq(mon.maxPp[2], 42, "Slot 2 max PP updated to 42")
-- Slot 1 has 3 in bits 0-1, Slot 2 has 2 in bits 2-3 (2 << 2 = 8): 3 | 8 = 11
checkEq(mon.ppBonusesPacked, 11, "ppBonusesPacked packed bitfield is 11")
-- Set current PP
MonOps.setPp(data, mon, 1, 20, 3)
checkEq(mon.pp[1], 20, "Slot 1 current PP set to 20")
-- Set current PP above max PP (clamped to maxPp 24)
MonOps.setPp(data, mon, 1, 50, 3)
checkEq(mon.pp[1], 24, "Slot 1 current PP clamped to max 24")
-- Set current PP below 0 (clamped to 0)
MonOps.setPp(data, mon, 1, -5, 3)
checkEq(mon.pp[1], 0, "Slot 1 current PP clamped to min 0")
-- Max all PP ups
MonOps.maxAllPpUps(data, mon, 3)
checkEq(MonOps.getPpUps(mon, 1), 3, "Slot 1 maxed to 3 PP Ups")
checkEq(MonOps.getPpUps(mon, 2), 3, "Slot 2 maxed to 3 PP Ups")
checkEq(mon.maxPp[1], 24, "Slot 1 max PP is 24")
checkEq(mon.maxPp[2], 48, "Slot 2 max PP is 48 (30 + 18)")
end
-- 11. Gen 1 / Gen 2 PP Ups and PP Editing
do
local mon = {
species = "PIKACHU",
level = 50,
moves = {
{ id = "THUNDERBOLT", pp = 15, maxPp = 15 },
{ id = "QUICK_ATTACK", pp = 30, maxPp = 30 },
}
}
local data = {
moves = {
THUNDERBOLT = { name = "THUNDERBOLT", pp = 15 },
QUICK_ATTACK = { name = "QUICK ATTACK", pp = 30 },
}
}
checkEq(MonOps.getPpUps(mon, 1), 0, "Gen 1 slot 1 PP Ups is 0")
MonOps.setPpUps(data, mon, 1, 3, 1)
checkEq(MonOps.getPpUps(mon, 1), 3, "Gen 1 slot 1 PP Ups set to 3")
checkEq(mon.moves[1].maxPp, 24, "Gen 1 slot 1 maxPp is 24")
MonOps.setPp(data, mon, 1, 10, 1)
checkEq(mon.moves[1].pp, 10, "Gen 1 slot 1 pp set to 10")
MonOps.maxAllPpUps(data, mon, 1)
checkEq(mon.moves[1].maxPp, 24, "Gen 1 slot 1 maxed to 24")
checkEq(mon.moves[2].maxPp, 48, "Gen 1 slot 2 maxed to 48")
end
-- 12. MonEditor.draw with Moves and PP controls smoke test
do
local S = {
data = {
moves = {
[85] = { name = "THUNDERBOLT", pp = 15 },
[98] = { name = "QUICK ATTACK", pp = 30 },
}
},
save = { version = "firered", engine = "game3", generation = 3 },
version = "firered",
editingMon = freshMonG3(),
inspectorScroll = 0,
}
S.editingMon.moves = { 85, 98, nil, nil }
S.editingMon.pp = { 15, 30, nil, nil }
S.editingMon.maxPp = { 15, 30, nil, nil }
S.editingMon.ppBonusesPacked = 0
local okWide, errWide = pcall(function()
MonEditor.draw(S, Kit, 10, 10, 600, 700)
end)
check(okWide, "MonEditor.draw renders Gen 3 with moves and PP controls (wide): " .. tostring(errWide))
local okNarrow, errNarrow = pcall(function()
MonEditor.draw(S, Kit, 10, 10, 400, 700)
end)
check(okNarrow, "MonEditor.draw renders Gen 3 with moves and PP controls (narrow): " .. tostring(errNarrow))
end
print(string.format("== EV/IV/PP Tests: %d passed, %d failed ==", passed, failed))
if failed > 0 then os.exit(1) end
+129 -59
View File
@@ -21,11 +21,20 @@ DENSITIES = {
}
SHELL_COLORS = {
"red": {"main": (230, 45, 55), "dark": (175, 25, 35), "light": (255, 90, 100)},
"blue": {"main": (35, 125, 235), "dark": (20, 85, 175), "light": (80, 165, 255)},
"yellow": {"main": (255, 205, 10), "dark": (210, 160, 0), "light": (255, 230, 80)},
"gold": {"main": (225, 170, 40), "dark": (170, 120, 20), "light": (245, 200, 80)},
"silver": {"main": (185, 190, 200), "dark": (130, 135, 145), "light": (225, 230, 240)},
# Gen 1 (GB)
"red": {"main": (230, 45, 55), "dark": (175, 25, 35), "light": (255, 90, 100), "shape": "gb"},
"blue": {"main": (35, 125, 235), "dark": (20, 85, 175), "light": (80, 165, 255), "shape": "gb"},
"yellow": {"main": (255, 205, 10), "dark": (210, 160, 0), "light": (255, 230, 80), "shape": "gb"},
# Gen 2 (GBC)
"gold": {"main": (225, 170, 40), "dark": (170, 120, 20), "light": (245, 200, 80), "shape": "gb"},
"silver": {"main": (185, 190, 200), "dark": (130, 135, 145), "light": (225, 230, 240), "shape": "gb"},
"crystal": {"main": (105, 195, 225), "dark": (60, 135, 165), "light": (180, 240, 255), "shape": "gb"},
# Gen 3 (GBA)
"firered": {"main": (235, 65, 35), "dark": (175, 30, 15), "light": (255, 115, 75), "shape": "gba"},
"leafgreen": {"main": (40, 175, 80), "dark": (20, 120, 45), "light": (85, 215, 125), "shape": "gba"},
"ruby": {"main": (195, 30, 50), "dark": (140, 15, 30), "light": (240, 75, 95), "shape": "gba"},
"sapphire": {"main": (25, 70, 185), "dark": (15, 40, 135), "light": (70, 120, 240), "shape": "gba"},
"emerald": {"main": (20, 155, 80), "dark": (10, 105, 50), "light": (65, 205, 125), "shape": "gba"},
}
def create_adaptive_foreground(size):
@@ -34,68 +43,129 @@ def create_adaptive_foreground(size):
return cover.resize((size, size), Image.Resampling.LANCZOS)
def render_3d_cartridge(version, size):
colors = SHELL_COLORS[version]
conf = SHELL_COLORS[version]
shape = conf.get("shape", "gb")
colors = conf
S = size * 4
# Transparent canvas so the cartridge sits directly on launcher's white circle plate
canvas = Image.new("RGBA", (S, S), (0, 0, 0, 0))
draw = ImageDraw.Draw(canvas)
# Tight padding to maximize cartridge size
pad_x = int(S * 0.05)
pad_y = int(S * 0.03)
cw = S - pad_x * 2
ch = S - pad_y * 2
# Soft drop shadow
shadow = Image.new("RGBA", (S, S), (0, 0, 0, 0))
sdraw = ImageDraw.Draw(shadow)
sdraw.rounded_rectangle([pad_x + 8, pad_y + 14, pad_x + cw + 8, pad_y + ch + 14],
radius=int(S * 0.05), fill=(0, 0, 0, 90))
shadow = shadow.filter(ImageFilter.GaussianBlur(int(S * 0.03)))
canvas.paste(shadow, (0, 0), shadow)
radius = int(S * 0.045)
depth = int(S * 0.03)
draw.rounded_rectangle([pad_x, pad_y + depth, pad_x + cw, pad_y + ch + depth],
radius=radius, fill=colors["dark"])
draw.rounded_rectangle([pad_x, pad_y, pad_x + cw, pad_y + ch],
radius=radius, fill=colors["main"])
notch_w = int(cw * 0.65)
notch_h = int(ch * 0.07)
notch_x = pad_x + (cw - notch_w) // 2
notch_y = pad_y + int(ch * 0.035)
draw.rounded_rectangle([notch_x, notch_y, notch_x + notch_w, notch_y + notch_h],
radius=int(notch_h * 0.4), fill=colors["dark"])
draw.rounded_rectangle([notch_x, notch_y - 2, notch_x + notch_w, notch_y + notch_h - 2],
radius=int(notch_h * 0.4), fill=colors["light"])
label_margin_x = int(cw * 0.09)
label_top_y = pad_y + int(ch * 0.20)
label_w = cw - label_margin_x * 2
label_h = int(ch * 0.70)
label_x = pad_x + label_margin_x
draw.rounded_rectangle([label_x - 3, label_top_y - 3, label_x + label_w + 3, label_top_y + label_h + 3],
radius=int(radius * 0.7), fill=colors["dark"])
label_path = os.path.join(ROOT, "assets", "labels", f"{version}.png")
if os.path.exists(label_path):
label_img = Image.open(label_path).convert("RGBA")
label_img = label_img.resize((label_w, label_h), Image.Resampling.LANCZOS)
if shape == "gba":
pad_x = int(S * 0.04)
cw = S - pad_x * 2
ch = int(cw * 0.64)
pad_y = (S - ch) // 2
mask = Image.new("L", (label_w, label_h), 0)
mdraw = ImageDraw.Draw(mask)
mdraw.rounded_rectangle([0, 0, label_w, label_h], radius=int(radius * 0.5), fill=255)
# Soft drop shadow
shadow = Image.new("RGBA", (S, S), (0, 0, 0, 0))
sdraw = ImageDraw.Draw(shadow)
sdraw.rounded_rectangle([pad_x + 6, pad_y + 12, pad_x + cw + 6, pad_y + ch + 12],
radius=int(S * 0.04), fill=(0, 0, 0, 95))
shadow = shadow.filter(ImageFilter.GaussianBlur(int(S * 0.03)))
canvas.paste(shadow, (0, 0), shadow)
canvas.paste(label_img, (label_x, label_top_y), mask)
radius = int(S * 0.04)
depth = int(S * 0.025)
# 3D lower bevel
draw.rounded_rectangle([pad_x, pad_y + depth, pad_x + cw, pad_y + ch + depth],
radius=radius, fill=colors["dark"])
# Main shell
draw.rounded_rectangle([pad_x, pad_y, pad_x + cw, pad_y + ch],
radius=radius, fill=colors["main"])
# Top ridge
top_lip_h = int(ch * 0.12)
draw.rounded_rectangle([pad_x + 4, pad_y + 2, pad_x + cw - 4, pad_y + top_lip_h],
radius=int(radius * 0.5), fill=colors["light"])
draw.rounded_rectangle([pad_x + 6, pad_y + 4, pad_x + cw - 6, pad_y + top_lip_h - 1],
radius=int(radius * 0.4), fill=colors["main"])
# Label area (landscape aspect ratio ~ 2:1)
label_margin_x = int(cw * 0.08)
label_top_y = pad_y + int(ch * 0.20)
label_w = cw - label_margin_x * 2
label_h = int(ch * 0.70)
label_x = pad_x + label_margin_x
# Label border/recess
draw.rounded_rectangle([label_x - 2, label_top_y - 2, label_x + label_w + 2, label_top_y + label_h + 2],
radius=int(radius * 0.5), fill=colors["dark"])
label_path = os.path.join(ROOT, "assets", "labels", f"{version}.png")
if os.path.exists(label_path):
label_img = Image.open(label_path).convert("RGBA")
label_img = label_img.resize((label_w, label_h), Image.Resampling.LANCZOS)
mask = Image.new("L", (label_w, label_h), 0)
mdraw = ImageDraw.Draw(mask)
mdraw.rounded_rectangle([0, 0, label_w, label_h], radius=int(radius * 0.4), fill=255)
canvas.paste(label_img, (label_x, label_top_y), mask)
else:
draw.rounded_rectangle([label_x, label_top_y, label_x + label_w, label_top_y + label_h],
radius=int(radius * 0.4), fill=(240, 240, 240, 255))
# Outer highlight
draw.rounded_rectangle([pad_x, pad_y, pad_x + cw, pad_y + ch],
radius=radius, outline=colors["light"], width=max(2, int(S * 0.008)))
else:
draw.rounded_rectangle([label_x, label_top_y, label_x + label_w, label_top_y + label_h],
radius=int(radius * 0.5), fill=(240, 240, 240, 255))
draw.rounded_rectangle([pad_x, pad_y, pad_x + cw, pad_y + ch],
radius=radius, outline=colors["light"], width=max(2, int(S * 0.008)))
# Tight padding to maximize cartridge size
pad_x = int(S * 0.05)
pad_y = int(S * 0.03)
cw = S - pad_x * 2
ch = S - pad_y * 2
# Soft drop shadow
shadow = Image.new("RGBA", (S, S), (0, 0, 0, 0))
sdraw = ImageDraw.Draw(shadow)
sdraw.rounded_rectangle([pad_x + 8, pad_y + 14, pad_x + cw + 8, pad_y + ch + 14],
radius=int(S * 0.05), fill=(0, 0, 0, 90))
shadow = shadow.filter(ImageFilter.GaussianBlur(int(S * 0.03)))
canvas.paste(shadow, (0, 0), shadow)
radius = int(S * 0.045)
depth = int(S * 0.03)
draw.rounded_rectangle([pad_x, pad_y + depth, pad_x + cw, pad_y + ch + depth],
radius=radius, fill=colors["dark"])
draw.rounded_rectangle([pad_x, pad_y, pad_x + cw, pad_y + ch],
radius=radius, fill=colors["main"])
notch_w = int(cw * 0.65)
notch_h = int(ch * 0.07)
notch_x = pad_x + (cw - notch_w) // 2
notch_y = pad_y + int(ch * 0.035)
draw.rounded_rectangle([notch_x, notch_y, notch_x + notch_w, notch_y + notch_h],
radius=int(notch_h * 0.4), fill=colors["dark"])
draw.rounded_rectangle([notch_x, notch_y - 2, notch_x + notch_w, notch_y + notch_h - 2],
radius=int(notch_h * 0.4), fill=colors["light"])
label_margin_x = int(cw * 0.09)
label_top_y = pad_y + int(ch * 0.20)
label_w = cw - label_margin_x * 2
label_h = int(ch * 0.70)
label_x = pad_x + label_margin_x
draw.rounded_rectangle([label_x - 3, label_top_y - 3, label_x + label_w + 3, label_top_y + label_h + 3],
radius=int(radius * 0.7), fill=colors["dark"])
label_path = os.path.join(ROOT, "assets", "labels", f"{version}.png")
if os.path.exists(label_path):
label_img = Image.open(label_path).convert("RGBA")
label_img = label_img.resize((label_w, label_h), Image.Resampling.LANCZOS)
mask = Image.new("L", (label_w, label_h), 0)
mdraw = ImageDraw.Draw(mask)
mdraw.rounded_rectangle([0, 0, label_w, label_h], radius=int(radius * 0.5), fill=255)
canvas.paste(label_img, (label_x, label_top_y), mask)
else:
draw.rounded_rectangle([label_x, label_top_y, label_x + label_w, label_top_y + label_h],
radius=int(radius * 0.5), fill=(240, 240, 240, 255))
draw.rounded_rectangle([pad_x, pad_y, pad_x + cw, pad_y + ch],
radius=radius, outline=colors["light"], width=max(2, int(S * 0.008)))
return canvas.resize((size, size), Image.Resampling.LANCZOS)
@@ -110,7 +180,7 @@ def main():
fg = create_adaptive_foreground(sizes["adaptive"])
fg.save(os.path.join(drawable_dir, "ic_launcher_foreground.png"), "PNG")
for ver in ("red", "blue", "yellow", "gold", "silver"):
for ver in SHELL_COLORS:
cart = render_3d_cartridge(ver, sizes["shortcut"])
cart.save(os.path.join(drawable_dir, f"ic_shortcut_{ver}.png"), "PNG")
+8 -7
View File
@@ -161,8 +161,8 @@ function Gen.bindGame3Data(data)
local okD, Dataset = pcall(require, "src.core.game3.dataset")
if okD and Dataset then
if Dataset.mountExtractRoots then pcall(Dataset.mountExtractRoots) end
local g3Maps = Dataset.buildMaps()
if g3Maps then
local okM, g3Maps = pcall(Dataset.buildMaps)
if okM and g3Maps then
local cache = Dataset.cache and Dataset.cache()
if cache and Dataset.attachMidLayouts then
pcall(Dataset.attachMidLayouts, g3Maps, cache)
@@ -181,8 +181,8 @@ function Gen.bindGame3Data(data)
pcall(Pokemon.install, nil)
data.pokemon = data.pokemon or {}
for id = 1, Pokemon.SPECIES_EGG - 1 do
local name = Pokemon.name(id)
if name and name ~= "??????????" and name ~= "" then
local okN, name = pcall(Pokemon.name, id)
if okN and name and name ~= "??????????" and name ~= "" then
local def = {
id = name,
name = name,
@@ -197,9 +197,10 @@ function Gen.bindGame3Data(data)
end
data.moves = data.moves or {}
local okB, BuiltinMoves = pcall(require, "src.core.game3.battle.builtin_moves")
for id = 1, 354 do
local mName = Pokemon.moveName(id)
local bMove = Pokemon.battleMove(id)
local bMove = Pokemon.battleMove(id) or (okB and BuiltinMoves and BuiltinMoves[id])
if mName and mName ~= "-------" and not mName:find("^MOVE %d+") then
local mDef = {
id = mName,
@@ -223,8 +224,8 @@ function Gen.bindGame3Data(data)
data.items[k] = iDef
end
for num = 1, 375 do
local info = ItemsData.info(num)
if info and info.name and info.name ~= "none" and info.name ~= "" then
local okInf, info = pcall(ItemsData.info, num)
if okInf and info and info.name and info.name ~= "none" and info.name ~= "" then
local normName = info.name:upper():gsub("[^A-Z0-9_]", "_"):gsub("_+", "_")
local iDef = { id = normName, name = info.name, pocket = info.pocket, itemId = num }
data.items[normName] = iDef
+198 -2
View File
@@ -108,7 +108,9 @@ function MonOps.recalc(data, mon, gen)
mon.gender = PokemonG3.gender and PokemonG3.gender(mon.speciesId or mon.species, mon.personality) or mon.gender
mon.ability = PokemonG3.abilityId and PokemonG3.abilityId(mon.speciesId or mon.species, mon.personality) or mon.ability
end
mon.hp = math.max(0, math.min(mon.hp or mon.maxHp, mon.maxHp))
if mon.maxHp then
mon.hp = math.max(0, math.min(mon.hp or mon.maxHp, mon.maxHp))
end
end
return
end
@@ -356,10 +358,155 @@ function MonOps.setMove(data, mon, slot, moveId)
moveId = mdef.moveId or numMove,
pp = basePp + currentUps * math.floor(basePp / 5),
ppUps = currentUps > 0 and currentUps or nil,
maxPp = basePp,
maxPp = basePp + currentUps * math.floor(basePp / 5),
}
end
function MonOps.getPpUps(mon, slot)
if type(mon) ~= "table" or not slot then return 0 end
if type(mon.ppBonusesPacked) == "number" then
local bit = require("bit")
return bit.band(bit.rshift(mon.ppBonusesPacked, (slot - 1) * 2), 3)
end
local mv = mon.moves and mon.moves[slot]
if type(mv) == "table" and mv.ppUps ~= nil then
return tonumber(mv.ppUps) or 0
end
if type(mon.ppBonuses) == "table" and mon.ppBonuses[slot] ~= nil then
return tonumber(mon.ppBonuses[slot]) or 0
end
return 0
end
function MonOps.getBasePp(data, mon, slot)
if type(mon) ~= "table" or not slot then return 10 end
local mv = mon.moves and mon.moves[slot]
local moveId = nil
if type(mv) == "table" then
moveId = mv.moveId or mv.id or mv.move
if mv.basePp then return tonumber(mv.basePp) end
elseif type(mv) == "number" then
moveId = mv
elseif type(mv) == "string" then
moveId = mv
end
if not moveId then return 10 end
local num = tonumber(moveId)
local mdef = data and data.moves and (data.moves[moveId] or (num and data.moves[num]))
if mdef and mdef.pp then return tonumber(mdef.pp) end
local okP, PokemonG3 = pcall(require, "src.core.game3.pokemon")
if okP and PokemonG3 then
if num and PokemonG3.battleMove then
local row = PokemonG3.battleMove(num)
if row and row.pp then return tonumber(row.pp) end
end
if num and PokemonG3.movePp then
local pp = PokemonG3.movePp(num)
if pp and pp > 0 then return pp end
end
end
local okB, BuiltinMoves = pcall(require, "src.core.game3.battle.builtin_moves")
if okB and BuiltinMoves then
if num and BuiltinMoves[num] and BuiltinMoves[num].pp then
return tonumber(BuiltinMoves[num].pp)
end
if type(moveId) == "string" then
local norm = moveId:upper():gsub("[^A-Z0-9]", " ")
for _, row in pairs(BuiltinMoves) do
if row.name and row.name:upper() == norm and row.pp then
return tonumber(row.pp)
end
end
end
end
return 10
end
function MonOps.calcMaxPp(basePp, ppUps)
basePp = math.max(1, tonumber(basePp) or 10)
ppUps = math.max(0, math.min(3, tonumber(ppUps) or 0))
return basePp + math.floor(basePp * 20 * ppUps / 100)
end
function MonOps.setPpUps(data, mon, slot, ppUps, gen)
if type(mon) ~= "table" or not slot then return end
ppUps = math.max(0, math.min(3, math.floor(tonumber(ppUps) or 0)))
local basePp = MonOps.getBasePp(data, mon, slot)
local newMaxPp = MonOps.calcMaxPp(basePp, ppUps)
local isG3 = (gen == 3) or (mon.speciesId ~= nil) or (mon.personality ~= nil) or (type(mon.ppBonusesPacked) == "number")
if isG3 then
local bit = require("bit")
local shift = (slot - 1) * 2
local packed = tonumber(mon.ppBonusesPacked) or 0
mon.ppBonusesPacked = bit.bor(bit.band(packed, bit.bnot(bit.lshift(3, shift))), bit.lshift(ppUps, shift))
mon.maxPp = mon.maxPp or {}
mon.maxPp[slot] = newMaxPp
mon.pp = mon.pp or {}
if mon.pp[slot] == nil or mon.pp[slot] > newMaxPp then
mon.pp[slot] = newMaxPp
end
else
local mv = mon.moves and mon.moves[slot]
if type(mv) == "table" then
mv.ppUps = ppUps > 0 and ppUps or nil
mv.maxPp = newMaxPp
if mv.pp == nil or mv.pp > newMaxPp then
mv.pp = newMaxPp
end
end
if type(mon.pp) == "table" and (mon.pp[slot] == nil or mon.pp[slot] > newMaxPp) then
mon.pp[slot] = newMaxPp
end
if type(mon.maxPp) == "table" then
mon.maxPp[slot] = newMaxPp
end
end
return ppUps, newMaxPp
end
function MonOps.setPp(data, mon, slot, value, gen)
if type(mon) ~= "table" or not slot then return end
local basePp = MonOps.getBasePp(data, mon, slot)
local ppUps = MonOps.getPpUps(mon, slot)
local maxPp = MonOps.calcMaxPp(basePp, ppUps)
local target = math.max(0, math.min(maxPp, math.floor(tonumber(value) or 0)))
local isG3 = (gen == 3) or (mon.speciesId ~= nil) or (mon.personality ~= nil) or (type(mon.ppBonusesPacked) == "number")
if isG3 then
mon.pp = mon.pp or {}
mon.pp[slot] = target
mon.maxPp = mon.maxPp or {}
mon.maxPp[slot] = maxPp
else
local mv = mon.moves and mon.moves[slot]
if type(mv) == "table" then
mv.pp = target
mv.maxPp = maxPp
end
if type(mon.pp) == "table" then
mon.pp[slot] = target
end
end
return target, maxPp
end
function MonOps.maxAllPpUps(data, mon, gen)
if type(mon) ~= "table" or not mon.moves then return end
for slot = 1, 4 do
if mon.moves[slot] then
MonOps.setPpUps(data, mon, slot, 3, gen)
local basePp = MonOps.getBasePp(data, mon, slot)
local maxPp = MonOps.calcMaxPp(basePp, 3)
MonOps.setPp(data, mon, slot, maxPp, gen)
end
end
end
-- HP DV is derived from the low bits of the other four (Stats.randomDVs).
function MonOps.syncHpDv(dvs)
if not dvs then return end
@@ -401,6 +548,55 @@ function MonOps.setDv(data, mon, key, value, gen)
MonOps.recalc(data, mon, gen)
end
local EV_KEYS = { "hp", "atk", "def", "spe", "spa", "spd" }
local IV_KEYS = { "hp", "atk", "def", "spe", "spa", "spd" }
function MonOps.setEv(data, mon, key, value, gen)
if type(mon) ~= "table" or not key then return end
mon.evs = mon.evs or { hp = 0, atk = 0, def = 0, spe = 0, spa = 0, spd = 0 }
local target = math.max(0, math.min(255, math.floor(tonumber(value) or 0)))
local otherTotal = 0
for _, k in ipairs(EV_KEYS) do
if k ~= key then
otherTotal = otherTotal + (tonumber(mon.evs[k]) or 0)
end
end
if otherTotal + target > 510 then
target = math.max(0, 510 - otherTotal)
end
mon.evs[key] = target
MonOps.recalc(data, mon, gen)
return target
end
function MonOps.clearEvs(data, mon, gen)
if type(mon) ~= "table" then return end
mon.evs = { hp = 0, atk = 0, def = 0, spe = 0, spa = 0, spd = 0 }
MonOps.recalc(data, mon, gen)
end
function MonOps.setIv(data, mon, key, value, gen)
if type(mon) ~= "table" or not key then return end
mon.ivs = mon.ivs or { hp = 31, atk = 31, def = 31, spe = 31, spa = 31, spd = 31 }
local target = math.max(0, math.min(31, math.floor(tonumber(value) or 0)))
mon.ivs[key] = target
mon.dvs = mon.dvs or {}
mon.dvs.attack = math.floor((mon.ivs.atk or 0) / 2)
mon.dvs.defense = math.floor((mon.ivs.def or 0) / 2)
mon.dvs.speed = math.floor((mon.ivs.spe or 0) / 2)
mon.dvs.special = math.floor(((mon.ivs.spa or 0) + (mon.ivs.spd or 0)) / 4)
mon.dvs.hp = math.floor((mon.ivs.hp or 0) / 2)
MonOps.recalc(data, mon, gen)
return target
end
function MonOps.maxIvs(data, mon, gen)
if type(mon) ~= "table" then return end
mon.ivs = { hp = 31, atk = 31, def = 31, spe = 31, spa = 31, spd = 31 }
mon.dvs = { attack = 15, defense = 15, speed = 15, special = 15, hp = 15 }
MonOps.recalc(data, mon, gen)
end
function MonOps.setSpecies(data, mon, species, gen)
if type(mon) ~= "table" then return end
local isG3 = (gen == 3) or (mon.speciesId ~= nil) or (mon.personality ~= nil)
+57
View File
@@ -476,6 +476,63 @@ function Ops.setDv(S, mon, key, value)
return Ops.mark(S, ("%s DV %d (HP DV now %d)"):format(key, mon.dvs[key], mon.dvs.hp))
end
function Ops.setEv(S, mon, key, value)
if not mon then return false end
mon.evs = mon.evs or { hp = 0, atk = 0, def = 0, spe = 0, spa = 0, spd = 0 }
local cur = tonumber(mon.evs[key]) or 0
local applied = MonOps.setEv(S.data, mon, key, value, Gen.ofState(S))
if applied == cur then
return Ops.say(S, ("%s EV is already %d"):format(key:upper(), cur))
end
return Ops.mark(S, ("%s EV set to %d"):format(key:upper(), applied or 0))
end
function Ops.clearEvs(S, mon)
if not mon then return false end
MonOps.clearEvs(S.data, mon, Gen.ofState(S))
return Ops.mark(S, ("Cleared EVs for %s"):format(mon.species or "Pokémon"))
end
function Ops.setIv(S, mon, key, value)
if not mon then return false end
mon.ivs = mon.ivs or { hp = 31, atk = 31, def = 31, spe = 31, spa = 31, spd = 31 }
local cur = tonumber(mon.ivs[key]) or 0
local applied = MonOps.setIv(S.data, mon, key, value, Gen.ofState(S))
if applied == cur then
return Ops.say(S, ("%s IV is already %d"):format(key:upper(), cur))
end
return Ops.mark(S, ("%s IV set to %d"):format(key:upper(), applied or 0))
end
function Ops.maxIvs(S, mon)
if not mon then return false end
MonOps.maxIvs(S.data, mon, Gen.ofState(S))
return Ops.mark(S, ("Maxed IVs (31) for %s"):format(mon.species or "Pokémon"))
end
function Ops.setPpUps(S, mon, slot, value)
if not mon or not slot or not (mon.moves and mon.moves[slot]) then return false end
local cur = MonOps.getPpUps(mon, slot)
local want = math.max(0, math.min(3, math.floor(tonumber(value) or 0)))
if want == cur then
return Ops.say(S, ("Slot %d PP Up is already %d"):format(slot, want))
end
local applied, maxPp = MonOps.setPpUps(S.data, mon, slot, want, Gen.ofState(S))
return Ops.mark(S, ("Slot %d PP Up set to %d (Max PP %d)"):format(slot, applied, maxPp))
end
function Ops.setPp(S, mon, slot, value)
if not mon or not slot or not (mon.moves and mon.moves[slot]) then return false end
local applied, maxPp = MonOps.setPp(S.data, mon, slot, value, Gen.ofState(S))
return Ops.mark(S, ("Slot %d PP set to %d/%d"):format(slot, applied or 0, maxPp or 0))
end
function Ops.maxAllPpUps(S, mon)
if not mon then return false end
MonOps.maxAllPpUps(S.data, mon, Gen.ofState(S))
return Ops.mark(S, ("Maxed PP Ups (3) and restored PP for %s"):format(mon.species or "Pokémon"))
end
-- Kept for tests and any keyboard path; the inspector opens the searchable
-- picker instead of walking the catalog one tap at a time.
function Ops.cycleMove(S, mon, slot)
+302 -30
View File
@@ -18,6 +18,7 @@
local Theme = require("Theme")
local Ops = require("Ops")
local MonOps = require("MonOps")
local PAL = Theme.PAL
local Gen = require("Gen")
@@ -132,6 +133,91 @@ local function levelRowWidth(Kit, mon)
+ 6 * s + Kit.textWidth("mono", ("EXP %d"):format(Gen.exp(mon)))
end
local STAT_KEYS_G3 = {
{ key = "hp", label = "HP" },
{ key = "atk", label = "ATTACK" },
{ key = "def", label = "DEFENSE" },
{ key = "spa", label = "SP. ATK" },
{ key = "spd", label = "SP. DEF" },
{ key = "spe", label = "SPEED" },
}
local function totalEvsOf(mon)
local evs = mon.evs or {}
local total = 0
for _, st in ipairs(STAT_KEYS_G3) do
total = total + (tonumber(evs[st.key]) or 0)
end
return total
end
local function drawIvRowsG3(S, Kit, mon, cx, rowY, colW, rowH, rowGap)
local s = Kit.scale
mon.ivs = mon.ivs or { hp = 31, atk = 31, def = 31, spe = 31, spa = 31, spd = 31 }
for i, st in ipairs(STAT_KEYS_G3) do
local ry = rowY + (i - 1) * (rowH + rowGap)
Theme.row(cx, ry, colW, rowH, 10 * s, 0.6)
local v = (mon.ivs and mon.ivs[st.key]) or 0
Kit.text("tiny", st.label, cx + 10 * s,
ry + (rowH - Kit.textHeight("tiny")) / 2, PAL.muted)
local btn = 26 * s
local btnX = cx + colW - 10 * s - 3 * btn - 18 * s
local meterX = cx + 66 * s
local meterW = math.max(16 * s, btnX - meterX - 34 * s)
Kit.meter(meterX, ry + (rowH - 8 * s) / 2, meterW, 8 * s, v / 31 * 100,
v >= 31 and PAL.green or (v >= 20 and PAL.blue or PAL.steel))
Kit.textRight("monoRow", tostring(v), meterX + meterW + 28 * s,
ry + (rowH - Kit.textHeight("monoRow")) / 2, PAL.heading)
if Kit.stepper(btnX, ry + (rowH - btn) / 2, btn, btn, "-") then
Ops.setIv(S, mon, st.key, v - 1)
end
if Kit.stepper(btnX + btn + 6 * s, ry + (rowH - btn) / 2, btn, btn, "+") then
Ops.setIv(S, mon, st.key, v + 1)
end
if Kit.button(btnX + 2 * btn + 12 * s, ry + (rowH - btn) / 2, btn, btn,
"31", { kind = "good", font = "micro", radius = 6 * s }) then
Ops.setIv(S, mon, st.key, 31)
end
end
end
local function drawEvRowsG3(S, Kit, mon, cx, rowY, colW, rowH, rowGap)
local s = Kit.scale
mon.evs = mon.evs or { hp = 0, atk = 0, def = 0, spe = 0, spa = 0, spd = 0 }
for i, st in ipairs(STAT_KEYS_G3) do
local ry = rowY + (i - 1) * (rowH + rowGap)
Theme.row(cx, ry, colW, rowH, 10 * s, 0.6)
local v = (mon.evs and mon.evs[st.key]) or 0
Kit.text("tiny", st.label, cx + 10 * s,
ry + (rowH - Kit.textHeight("tiny")) / 2, PAL.muted)
local btn = 24 * s
local rightX = cx + colW - 10 * s
local b252W = 28 * s
local b252X = rightX - b252W
local p1X = b252X - 6 * s - btn
local m1X = p1X - 6 * s - btn
local meterX = cx + 66 * s
local meterW = math.max(16 * s, m1X - meterX - 34 * s)
Kit.meter(meterX, ry + (rowH - 8 * s) / 2, meterW, 8 * s, v / 255 * 100,
v >= 252 and PAL.green or (v >= 100 and PAL.blue or PAL.steel))
Kit.textRight("monoRow", tostring(v), meterX + meterW + 28 * s,
ry + (rowH - Kit.textHeight("monoRow")) / 2, PAL.heading)
if Kit.stepper(m1X, ry + (rowH - btn) / 2, btn, btn, "-") then
Ops.setEv(S, mon, st.key, v - 1)
end
if Kit.stepper(p1X, ry + (rowH - btn) / 2, btn, btn, "+") then
Ops.setEv(S, mon, st.key, v + 1)
end
if Kit.button(b252X, ry + (rowH - btn) / 2, b252W, btn,
"252", { kind = "good", font = "micro", radius = 6 * s }) then
Ops.setEv(S, mon, st.key, 252)
end
end
end
local function drawDvRows(S, Kit, mon, cx, rowY, colW, rowH, rowGap)
local s = Kit.scale
mon.dvs = mon.dvs or { attack = 0, defense = 0, speed = 0, special = 0, hp = 0 }
@@ -229,36 +315,83 @@ local function drawMoveRows(S, Kit, mon, rightX, rowY, colW, rowH, rowGap)
Theme.row(rightX, ry, colW, rowH, 10 * s, 0.6)
local mv = mon.moves and mon.moves[slot]
local clear = Kit.tapMin()
local clearX = rightX + colW - 10 * s - clear
local clearX = rightX + colW - 8 * s - clear
local mvId = nil
local mvPp = 0
if type(mv) == "table" then
mvId = (S.data and S.data.moves and S.data.moves[mv.id] and S.data.moves[mv.id].name) or mv.id or mv.name
mvPp = mv.pp or 0
elseif type(mv) == "number" then
mvId = S.data and S.data.moves and S.data.moves[mv] and S.data.moves[mv].name
mvPp = mon.pp and mon.pp[slot] or 0
if not mvId then
local okP, PokemonG3 = pcall(require, "src.core.game3.pokemon")
if okP and PokemonG3 and PokemonG3.moveName then
mvId = PokemonG3.moveName(mv)
end
end
elseif type(mv) == "string" then
mvId = mv
end
local ppText = mv and ("PP %d"):format(mvPp) or ""
local ppW = Kit.textWidth("tiny", ppText)
Kit.text("mono", tostring(slot), rightX + 10 * s,
ry + (rowH - Kit.textHeight("mono")) / 2, PAL.faint)
local nameX = rightX + 28 * s
local nameW2 = math.max(20 * s, clearX - 12 * s - ppW - nameX)
Kit.text("monoRow", Kit.ellipsize("monoRow", mvId or "-- --", nameW2),
nameX, ry + (rowH - Kit.textHeight("monoRow")) / 2,
mv and PAL.text or PAL.faint)
Kit.textRight("tiny", ppText, clearX - 10 * s,
ry + (rowH - Kit.textHeight("tiny")) / 2, PAL.caption)
-- the row body opens the searchable picker; the x empties the slot
if Kit.press(rightX, ry, clearX - rightX - 4 * s, rowH) then
Ops.openMovePicker(S, Kit, slot)
end
if Kit.button(clearX, ry + (rowH - clear) / 2, clear, clear, "x",
{ kind = "danger", font = "tiny", radius = 6 * s }) then
Ops.clearMove(S, mon, slot)
if mv then
local basePp = MonOps.getBasePp(S.data, mon, slot)
local ppUps = MonOps.getPpUps(mon, slot)
local maxPp = (mon.maxPp and mon.maxPp[slot]) or (type(mv) == "table" and mv.maxPp) or MonOps.calcMaxPp(basePp, ppUps)
local currentPp = (type(mv) == "table" and mv.pp) or (mon.pp and mon.pp[slot]) or maxPp
local ppUpW = 44 * s
local btnH = 22 * s
local ppUpX = clearX - 6 * s - ppUpW
local plusX = ppUpX - 4 * s - btnH
local minusX = plusX - 4 * s - btnH
-- PP Up button
local upLabel = ("+PP %d"):format(ppUps)
if Kit.button(ppUpX, ry + (rowH - btnH) / 2, ppUpW, btnH, upLabel,
{ kind = ppUps > 0 and "accent" or "ghost", font = "micro", radius = 5 * s }) then
Ops.setPpUps(S, mon, slot, (ppUps + 1) % 4)
end
-- Steppers for current PP
if Kit.stepper(minusX, ry + (rowH - btnH) / 2, btnH, btnH, "-", { font = "small" }) then
Ops.setPp(S, mon, slot, currentPp - 1)
end
if Kit.stepper(plusX, ry + (rowH - btnH) / 2, btnH, btnH, "+", { font = "small" }) then
Ops.setPp(S, mon, slot, currentPp + 1)
end
local ppText = ("%d/%d"):format(currentPp, maxPp)
local ppW = Kit.textWidth("tiny", ppText)
local ppColor = currentPp == 0 and PAL.red or (currentPp < maxPp and PAL.caption or PAL.text)
Kit.textRight("tiny", ppText, minusX - 6 * s,
ry + (rowH - Kit.textHeight("tiny")) / 2, ppColor)
local nameX = rightX + 28 * s
local nameW = math.max(20 * s, minusX - 10 * s - ppW - nameX)
Kit.text("monoRow", Kit.ellipsize("monoRow", mvId or "-- --", nameW),
nameX, ry + (rowH - Kit.textHeight("monoRow")) / 2, PAL.text)
-- Press on move name opens picker
if Kit.press(rightX, ry, math.max(0, minusX - 8 * s - rightX), rowH) then
Ops.openMovePicker(S, Kit, slot)
end
-- Clear move button
if Kit.button(clearX, ry + (rowH - clear) / 2, clear, clear, "x",
{ kind = "danger", font = "tiny", radius = 6 * s }) then
Ops.clearMove(S, mon, slot)
end
else
local nameX = rightX + 28 * s
local nameW = math.max(20 * s, clearX - 8 * s - nameX)
Kit.text("monoRow", Kit.ellipsize("monoRow", "-- --", nameW),
nameX, ry + (rowH - Kit.textHeight("monoRow")) / 2, PAL.faint)
if Kit.press(rightX, ry, clearX - rightX - 4 * s, rowH) then
Ops.openMovePicker(S, Kit, slot)
end
end
end
end
@@ -307,12 +440,22 @@ function MonEditor.draw(S, Kit, x, y, w, h)
headerH = math.max(sprite, titleH) + 12 * s + 28 * s
end
local colRowsH = 4 * (rowH + rowGap) - rowGap
local isG3 = Gen.ofState(S) == 3
local colRowsH = (isG3 and 6 or 4) * (rowH + rowGap) - rowGap
local movesRowsH = 4 * (rowH + rowGap) - rowGap
local colsH
if narrow then
colsH = (capH + 10 * s + colRowsH) * 2 + 14 * s + 10 * s + actH
if isG3 then
if narrow then
colsH = (capH + 10 * s + colRowsH) * 2 + (capH + 10 * s + movesRowsH) + 28 * s + 10 * s + actH
else
colsH = (capH + 10 * s + colRowsH) + 14 * s + (capH + 10 * s + movesRowsH) + 12 * s + actH
end
else
colsH = capH + 10 * s + colRowsH + 12 * s + actH
if narrow then
colsH = (capH + 10 * s + colRowsH) * 2 + 14 * s + 10 * s + actH
else
colsH = capH + 10 * s + colRowsH + 12 * s + actH
end
end
-- the nickname section: a caption line (with the Clear button on it) plus
-- the field + Set row
@@ -414,7 +557,11 @@ function MonEditor.draw(S, Kit, x, y, w, h)
-- ------------------------------------------------------- derived stats
local statsY = nickY + capH + 10 * s + nickFieldH + 18 * s
Kit.caption(cx, statsY, "STATS . recalculated from level + DVs")
if isG3 then
Kit.caption(cx, statsY, "STATS . recalculated from level + IVs + EVs + nature")
else
Kit.caption(cx, statsY, "STATS . recalculated from level + DVs")
end
statsY = statsY + capH + 10 * s
local STAT_KEYS = Gen.ofState(S) >= 2 and STAT_KEYS_G2 or STAT_KEYS_G1
local gap = 12 * s
@@ -451,9 +598,10 @@ function MonEditor.draw(S, Kit, x, y, w, h)
extraY = extraY + row + 6 * s
-- Nature and Friendship
local SummaryData = require("src.core.game3.summary_data")
local SummaryData = package.loaded["src.core.game3.summary_data"] or require("src.core.game3.summary_data")
local natureIdx = mon.nature or ((mon.personality or 0) % 25)
local natureName = SummaryData.NATURES[natureIdx]
local okN, nName = pcall(function() return SummaryData.NATURES[natureIdx] end)
local natureName = (okN and nName) or ("Nature " .. natureIdx)
Kit.text("tiny", "NATURE " .. natureName, cx, extraY, PAL.text)
if Kit.stepper(cx + 140 * s, extraY, 28 * s, row, "<", { font = "small" }) then
Ops.setNature(S, mon, (natureIdx - 1 + 25) % 25)
@@ -534,7 +682,117 @@ function MonEditor.draw(S, Kit, x, y, w, h)
end
colY = extraY
end
if narrow then
if isG3 then
local ivEvRowsH = 6 * (rowH + rowGap) - rowGap
local mRowsH = 4 * (rowH + rowGap) - rowGap
local totalEvs = totalEvsOf(mon)
if narrow then
-- 1. IVs section
Kit.caption(cx, colY, "IVs (0-31)")
local maxIvW = 84 * s
local maxIvH = 22 * s
if Kit.button(cx + inner - maxIvW, colY + (capH - maxIvH) / 2, maxIvW, maxIvH,
"Max all (31)", { kind = "good", font = "micro", radius = 6 * s }) then
Ops.maxIvs(S, mon)
end
local ivRowY = colY + capH + 10 * s
drawIvRowsG3(S, Kit, mon, cx, ivRowY, inner, rowH, rowGap)
-- 2. EVs section
local evY = ivRowY + ivEvRowsH + 14 * s
Kit.caption(cx, evY, "EVs (0-255)")
local clearEvW = 54 * s
local clearEvH = 22 * s
if Kit.button(cx + inner - clearEvW, evY + (capH - clearEvH) / 2, clearEvW, clearEvH,
"Clear", { kind = "danger", font = "micro", radius = 6 * s }) then
Ops.clearEvs(S, mon)
end
local evTotalText = ("TOTAL %d / 510"):format(totalEvs)
Kit.textRight("tiny", evTotalText, cx + inner - clearEvW - 10 * s,
evY + (capH - Kit.textHeight("tiny")) / 2, totalEvs >= 510 and PAL.green or PAL.caption)
local evRowY = evY + capH + 10 * s
drawEvRowsG3(S, Kit, mon, cx, evRowY, inner, rowH, rowGap)
-- 3. Moves section
local movesY = evRowY + ivEvRowsH + 14 * s
Kit.caption(cx, movesY, "MOVES")
local maxPpW = 84 * s
local maxPpH = 22 * s
if Kit.button(cx + inner - maxPpW, movesY + (capH - maxPpH) / 2, maxPpW, maxPpH,
"Max all PP", { kind = "good", font = "micro", radius = 6 * s }) then
Ops.maxAllPpUps(S, mon)
end
Kit.textRight("tiny", "click a slot to search", cx + inner - maxPpW - 10 * s,
movesY + (capH - Kit.textHeight("tiny")) / 2, PAL.caption)
local mRowY = movesY + capH + 10 * s
drawMoveRows(S, Kit, mon, cx, mRowY, inner, rowH, rowGap)
local actY = mRowY + mRowsH + 10 * s
local actW = (inner - 10 * s) / 2
if Kit.button(cx, actY, actW, actH, "Reset to learnset",
{ font = "small", radius = 9 * s }) then
Ops.resetMoves(S, mon)
end
if Kit.button(cx + actW + 10 * s, actY, actW, actH, "Full heal",
{ kind = "good", font = "small", radius = 9 * s }) then
Ops.healMon(S, mon)
end
else
local colGap = 18 * s
local colW = (inner - colGap) / 2
local rightX = cx + colW + colGap
-- Left: IVs
Kit.caption(cx, colY, "IVs (0-31)")
local maxIvW = 84 * s
local maxIvH = 22 * s
if Kit.button(cx + colW - maxIvW, colY + (capH - maxIvH) / 2, maxIvW, maxIvH,
"Max all (31)", { kind = "good", font = "micro", radius = 6 * s }) then
Ops.maxIvs(S, mon)
end
local ivRowY = colY + capH + 10 * s
drawIvRowsG3(S, Kit, mon, cx, ivRowY, colW, rowH, rowGap)
-- Right: EVs
Kit.caption(rightX, colY, "EVs (0-255)")
local clearEvW = 54 * s
local clearEvH = 22 * s
if Kit.button(rightX + colW - clearEvW, colY + (capH - clearEvH) / 2, clearEvW, clearEvH,
"Clear", { kind = "danger", font = "micro", radius = 6 * s }) then
Ops.clearEvs(S, mon)
end
local evTotalText = ("TOTAL %d / 510"):format(totalEvs)
Kit.textRight("tiny", evTotalText, rightX + colW - clearEvW - 10 * s,
colY + (capH - Kit.textHeight("tiny")) / 2, totalEvs >= 510 and PAL.green or PAL.caption)
drawEvRowsG3(S, Kit, mon, rightX, ivRowY, colW, rowH, rowGap)
-- Moves Section below IVs and EVs
local movesY = ivRowY + ivEvRowsH + 14 * s
Kit.caption(cx, movesY, "MOVES")
local maxPpW = 84 * s
local maxPpH = 22 * s
if Kit.button(cx + inner - maxPpW, movesY + (capH - maxPpH) / 2, maxPpW, maxPpH,
"Max all PP", { kind = "good", font = "micro", radius = 6 * s }) then
Ops.maxAllPpUps(S, mon)
end
Kit.textRight("tiny", "click a slot to search", cx + inner - maxPpW - 10 * s,
movesY + (capH - Kit.textHeight("tiny")) / 2, PAL.caption)
local mRowY = movesY + capH + 10 * s
drawMoveRows(S, Kit, mon, cx, mRowY, inner, rowH, rowGap)
local actY = mRowY + mRowsH + 12 * s
local actW = (inner - 10 * s) / 2
if Kit.button(cx, actY, actW, actH, "Reset to learnset",
{ font = "small", radius = 9 * s }) then
Ops.resetMoves(S, mon)
end
if Kit.button(cx + actW + 10 * s, actY, actW, actH, "Full heal",
{ kind = "good", font = "small", radius = 9 * s }) then
Ops.healMon(S, mon)
end
end
elseif narrow then
-- stacked: DVs first, then moves, then the two actions side by side at
-- full width (#715)
Kit.caption(cx, colY, "DVs")
@@ -545,7 +803,14 @@ function MonEditor.draw(S, Kit, x, y, w, h)
local movesY = rowY + colRowsH + 14 * s
Kit.caption(cx, movesY, "MOVES")
Kit.textRight("tiny", "click a slot to search", cx + inner, movesY, PAL.caption)
local maxPpW = 84 * s
local maxPpH = 22 * s
if Kit.button(cx + inner - maxPpW, movesY + (capH - maxPpH) / 2, maxPpW, maxPpH,
"Max all PP", { kind = "good", font = "micro", radius = 6 * s }) then
Ops.maxAllPpUps(S, mon)
end
Kit.textRight("tiny", "click a slot to search", cx + inner - maxPpW - 10 * s,
movesY + (capH - Kit.textHeight("tiny")) / 2, PAL.caption)
local mRowY = movesY + capH + 10 * s
drawMoveRows(S, Kit, mon, cx, mRowY, inner, rowH, rowGap)
@@ -568,7 +833,14 @@ function MonEditor.draw(S, Kit, x, y, w, h)
Kit.textRight("tiny", ("HP DV auto-derived . %d"):format((mon.dvs and mon.dvs.hp) or 0),
cx + colW, colY, PAL.caption)
Kit.caption(rightX, colY, "MOVES")
Kit.textRight("tiny", "click a slot to search", rightX + colW, colY, PAL.caption)
local maxPpW = 84 * s
local maxPpH = 22 * s
if Kit.button(rightX + colW - maxPpW, colY + (capH - maxPpH) / 2, maxPpW, maxPpH,
"Max all PP", { kind = "good", font = "micro", radius = 6 * s }) then
Ops.maxAllPpUps(S, mon)
end
Kit.textRight("tiny", "click a slot to search", rightX + colW - maxPpW - 10 * s,
colY + (capH - Kit.textHeight("tiny")) / 2, PAL.caption)
local rowY = colY + capH + 10 * s
drawDvRows(S, Kit, mon, cx, rowY, colW, rowH, rowGap)