Merge pull request #2491 from bryanthaboi/dev
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 181 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
|
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:
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -139,6 +139,7 @@ return {
|
||||
"egg_extract",
|
||||
"battle_anim_extract",
|
||||
"battle_ai_extract",
|
||||
"map_preview_extract",
|
||||
"credits_extract",
|
||||
"league_extract",
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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([[
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||