diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png index a64894a2..4c59d063 100644 Binary files a/mobile/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png and b/mobile/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_blue.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_blue.png index 292907ed..356b2411 100644 Binary files a/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_blue.png and b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_blue.png differ diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_crystal.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_crystal.png new file mode 100644 index 00000000..eb31d57e Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_crystal.png differ diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_emerald.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_emerald.png new file mode 100644 index 00000000..e3b77a48 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_emerald.png differ diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_firered.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_firered.png new file mode 100644 index 00000000..25629924 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_firered.png differ diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_gold.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_gold.png index a9fb6040..00628ab3 100644 Binary files a/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_gold.png and b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_gold.png differ diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_leafgreen.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_leafgreen.png new file mode 100644 index 00000000..27d14d96 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_leafgreen.png differ diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_red.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_red.png index c2b97381..5c6c7b1f 100644 Binary files a/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_red.png and b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_red.png differ diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_ruby.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_ruby.png new file mode 100644 index 00000000..aa3c58ef Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_ruby.png differ diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_sapphire.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_sapphire.png new file mode 100644 index 00000000..6dadc548 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_sapphire.png differ diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_silver.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_silver.png index 30a113e3..7158db46 100644 Binary files a/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_silver.png and b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_silver.png differ diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_yellow.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_yellow.png index 424d1614..6dd51e9f 100644 Binary files a/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_yellow.png and b/mobile/android/app/src/main/res/drawable-hdpi/ic_shortcut_yellow.png differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png index 9179b6ce..6baa222a 100644 Binary files a/mobile/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png and b/mobile/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_blue.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_blue.png index 253cac46..bc369d95 100644 Binary files a/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_blue.png and b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_blue.png differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_crystal.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_crystal.png new file mode 100644 index 00000000..0ba7c2c0 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_crystal.png differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_emerald.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_emerald.png new file mode 100644 index 00000000..f1459282 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_emerald.png differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_firered.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_firered.png new file mode 100644 index 00000000..6c0b6ac6 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_firered.png differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_gold.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_gold.png index 1df17543..8c661216 100644 Binary files a/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_gold.png and b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_gold.png differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_leafgreen.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_leafgreen.png new file mode 100644 index 00000000..cc39f2d0 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_leafgreen.png differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_red.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_red.png index 591974c8..a440b95a 100644 Binary files a/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_red.png and b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_red.png differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_ruby.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_ruby.png new file mode 100644 index 00000000..4649613e Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_ruby.png differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_sapphire.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_sapphire.png new file mode 100644 index 00000000..30904ac0 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_sapphire.png differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_silver.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_silver.png index c4d95a58..d2b66757 100644 Binary files a/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_silver.png and b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_silver.png differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_yellow.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_yellow.png index d61705ab..a8b97a63 100644 Binary files a/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_yellow.png and b/mobile/android/app/src/main/res/drawable-mdpi/ic_shortcut_yellow.png differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png index 768b3abc..1560ebb0 100644 Binary files a/mobile/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png and b/mobile/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_blue.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_blue.png index c77b0413..c7c5d919 100644 Binary files a/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_blue.png and b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_blue.png differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_crystal.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_crystal.png new file mode 100644 index 00000000..a2c96022 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_crystal.png differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_emerald.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_emerald.png new file mode 100644 index 00000000..3dd05fc5 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_emerald.png differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_firered.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_firered.png new file mode 100644 index 00000000..7a649972 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_firered.png differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_gold.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_gold.png index b1927bba..6326f50a 100644 Binary files a/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_gold.png and b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_gold.png differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_leafgreen.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_leafgreen.png new file mode 100644 index 00000000..874c37ee Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_leafgreen.png differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_red.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_red.png index 4e30e872..535b01dc 100644 Binary files a/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_red.png and b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_red.png differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_ruby.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_ruby.png new file mode 100644 index 00000000..a3e944b5 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_ruby.png differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_sapphire.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_sapphire.png new file mode 100644 index 00000000..17f4ae47 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_sapphire.png differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_silver.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_silver.png index 7655ee86..bda72b5c 100644 Binary files a/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_silver.png and b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_silver.png differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_yellow.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_yellow.png index 35aadb5c..c4a01e0b 100644 Binary files a/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_yellow.png and b/mobile/android/app/src/main/res/drawable-xhdpi/ic_shortcut_yellow.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png index 2cf2245d..77d9ee5d 100644 Binary files a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png and b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_blue.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_blue.png index 595c1747..2c24e8d3 100644 Binary files a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_blue.png and b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_blue.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_crystal.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_crystal.png new file mode 100644 index 00000000..5049d9f0 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_crystal.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_emerald.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_emerald.png new file mode 100644 index 00000000..fb872045 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_emerald.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_firered.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_firered.png new file mode 100644 index 00000000..87e42ef8 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_firered.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_gold.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_gold.png index 93596378..b252436e 100644 Binary files a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_gold.png and b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_gold.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_leafgreen.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_leafgreen.png new file mode 100644 index 00000000..33497148 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_leafgreen.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_red.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_red.png index 4efbcd33..f41952be 100644 Binary files a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_red.png and b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_red.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_ruby.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_ruby.png new file mode 100644 index 00000000..cd7daf75 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_ruby.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_sapphire.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_sapphire.png new file mode 100644 index 00000000..90232fc9 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_sapphire.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_silver.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_silver.png index e6ea7d4d..c21477a2 100644 Binary files a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_silver.png and b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_silver.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_yellow.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_yellow.png index 7afb90e5..fe044f1f 100644 Binary files a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_yellow.png and b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_shortcut_yellow.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png index e8e1dc34..e17eb9b3 100644 Binary files a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png and b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_blue.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_blue.png index a594a435..c1a77ab1 100644 Binary files a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_blue.png and b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_blue.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_crystal.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_crystal.png new file mode 100644 index 00000000..b798ca91 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_crystal.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_emerald.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_emerald.png new file mode 100644 index 00000000..143b80cb Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_emerald.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_firered.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_firered.png new file mode 100644 index 00000000..d09ba029 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_firered.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_gold.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_gold.png index a1d10860..aa7f14bf 100644 Binary files a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_gold.png and b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_gold.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_leafgreen.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_leafgreen.png new file mode 100644 index 00000000..777a4b66 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_leafgreen.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_red.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_red.png index 44d26881..53bb788d 100644 Binary files a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_red.png and b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_red.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_ruby.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_ruby.png new file mode 100644 index 00000000..385cdc98 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_ruby.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_sapphire.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_sapphire.png new file mode 100644 index 00000000..d25dc2c5 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_sapphire.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_silver.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_silver.png index b0efbbe1..580ecd70 100644 Binary files a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_silver.png and b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_silver.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_yellow.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_yellow.png index 825a5760..d35edc80 100644 Binary files a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_yellow.png and b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_shortcut_yellow.png differ diff --git a/mobile/android/app/src/main/res/values/colors.xml b/mobile/android/app/src/main/res/values/colors.xml index 98c05cbb..882a7bf5 100644 --- a/mobile/android/app/src/main/res/values/colors.xml +++ b/mobile/android/app/src/main/res/values/colors.xml @@ -9,4 +9,10 @@ #FDD835 #D4AF37 #BEC6D2 + #6EC3DF + #EB4123 + #28AF50 + #C31E32 + #1946B9 + #149B50 diff --git a/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java b/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java index cd41fb1f..fb99ef0c 100644 --- a/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java +++ b/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java @@ -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: diff --git a/src/core/game3/battle/builtin_moves.lua b/src/core/game3/battle/builtin_moves.lua new file mode 100644 index 00000000..61e89d7b --- /dev/null +++ b/src/core/game3/battle/builtin_moves.lua @@ -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 diff --git a/src/core/game3/dataset.lua b/src/core/game3/dataset.lua index 60db21d3..f97be285 100644 --- a/src/core/game3/dataset.lua +++ b/src/core/game3/dataset.lua @@ -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() diff --git a/src/core/game3/pokemon.lua b/src/core/game3/pokemon.lua index 1fabfc66..915cc296 100644 --- a/src/core/game3/pokemon.lua +++ b/src/core/game3/pokemon.lua @@ -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) diff --git a/src/core/game3/profiles/firered.lua b/src/core/game3/profiles/firered.lua index 8a6274ad..67bff26c 100644 --- a/src/core/game3/profiles/firered.lua +++ b/src/core/game3/profiles/firered.lua @@ -139,6 +139,7 @@ return { "egg_extract", "battle_anim_extract", "battle_ai_extract", + "map_preview_extract", "credits_extract", "league_extract", }, diff --git a/src/import/CacheFs.lua b/src/import/CacheFs.lua index 6378c325..96ae1f1e 100644 --- a/src/import/CacheFs.lua +++ b/src/import/CacheFs.lua @@ -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 diff --git a/src/import/ExtractThread.lua b/src/import/ExtractThread.lua index f0960e88..cf0ea0ea 100644 --- a/src/import/ExtractThread.lua +++ b/src/import/ExtractThread.lua @@ -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. diff --git a/src/import/RomExtractorGen3.lua b/src/import/RomExtractorGen3.lua index 732451ef..0bacae2f 100644 --- a/src/import/RomExtractorGen3.lua +++ b/src/import/RomExtractorGen3.lua @@ -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 diff --git a/src/import/gba/bag_chrome_extract.lua b/src/import/gba/bag_chrome_extract.lua index 6288d2d5..4da81061 100644 --- a/src/import/gba/bag_chrome_extract.lua +++ b/src/import/gba/bag_chrome_extract.lua @@ -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 diff --git a/src/import/gba/battle_anim_extract.lua b/src/import/gba/battle_anim_extract.lua index b88133b1..141db4a1 100644 --- a/src/import/gba/battle_anim_extract.lua +++ b/src/import/gba/battle_anim_extract.lua @@ -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 diff --git a/src/import/gba/extract_island1.lua b/src/import/gba/extract_island1.lua index 5916d385..cf620b8c 100644 --- a/src/import/gba/extract_island1.lua +++ b/src/import/gba/extract_island1.lua @@ -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) diff --git a/src/import/gba/extract_worker.lua b/src/import/gba/extract_worker.lua index 896c34a2..6a183e80 100644 --- a/src/import/gba/extract_worker.lua +++ b/src/import/gba/extract_worker.lua @@ -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 diff --git a/src/import/gba/lz77.lua b/src/import/gba/lz77.lua index a21a040c..c16238d0 100644 --- a/src/import/gba/lz77.lua +++ b/src/import/gba/lz77.lua @@ -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) diff --git a/src/import/gba/map_tree_extract.lua b/src/import/gba/map_tree_extract.lua index d0f81f12..50f4b49d 100644 --- a/src/import/gba/map_tree_extract.lua +++ b/src/import/gba/map_tree_extract.lua @@ -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 diff --git a/src/import/gba/pokemon_extract.lua b/src/import/gba/pokemon_extract.lua index 5166de76..6fead337 100644 --- a/src/import/gba/pokemon_extract.lua +++ b/src/import/gba/pokemon_extract.lua @@ -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 diff --git a/src/import/gba/rom.lua b/src/import/gba/rom.lua index 84c7f2bb..0a593c41 100644 --- a/src/import/gba/rom.lua +++ b/src/import/gba/rom.lua @@ -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 diff --git a/src/import/gba/tileset.lua b/src/import/gba/tileset.lua index 8256e813..cf473576 100644 --- a/src/import/gba/tileset.lua +++ b/src/import/gba/tileset.lua @@ -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 diff --git a/src/import/gba/tm_case_extract.lua b/src/import/gba/tm_case_extract.lua index eae101ed..423f34da 100644 --- a/src/import/gba/tm_case_extract.lua +++ b/src/import/gba/tm_case_extract.lua @@ -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([[ diff --git a/src/import/gba/versions.lua b/src/import/gba/versions.lua index 52f3a311..c2c239b5 100644 --- a/src/import/gba/versions.lua +++ b/src/import/gba/versions.lua @@ -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) diff --git a/tests/engine/android_shortcuts_payload_test.lua b/tests/engine/android_shortcuts_payload_test.lua index d7ea27d5..768eadbf 100644 --- a/tests/engine/android_shortcuts_payload_test.lua +++ b/tests/engine/android_shortcuts_payload_test.lua @@ -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" diff --git a/tests/game3_shop_bag_chrome_test.lua b/tests/game3_shop_bag_chrome_test.lua index 82fc0291..10baafd3 100644 --- a/tests/game3_shop_bag_chrome_test.lua +++ b/tests/game3_shop_bag_chrome_test.lua @@ -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") diff --git a/tests/game3_tm_case_berry_pouch_extract_test.lua b/tests/game3_tm_case_berry_pouch_extract_test.lua index 7294b275..405c753e 100644 --- a/tests/game3_tm_case_berry_pouch_extract_test.lua +++ b/tests/game3_tm_case_berry_pouch_extract_test.lua @@ -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) diff --git a/tests/lz77_test.lua b/tests/lz77_test.lua index eac63a7d..bc7d69aa 100644 --- a/tests/lz77_test.lua +++ b/tests/lz77_test.lua @@ -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 diff --git a/tests/save_editor_gen3_ev_iv_tests.lua b/tests/save_editor_gen3_ev_iv_tests.lua new file mode 100644 index 00000000..7622d71d --- /dev/null +++ b/tests/save_editor_gen3_ev_iv_tests.lua @@ -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 + diff --git a/tools/generate_android_icons.py b/tools/generate_android_icons.py index 5e317383..f3f071b7 100644 --- a/tools/generate_android_icons.py +++ b/tools/generate_android_icons.py @@ -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") diff --git a/tools/save-editor/Gen.lua b/tools/save-editor/Gen.lua index b640f31d..7ae331e0 100644 --- a/tools/save-editor/Gen.lua +++ b/tools/save-editor/Gen.lua @@ -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 diff --git a/tools/save-editor/MonOps.lua b/tools/save-editor/MonOps.lua index e156c7e0..a5cb7999 100644 --- a/tools/save-editor/MonOps.lua +++ b/tools/save-editor/MonOps.lua @@ -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) diff --git a/tools/save-editor/Ops.lua b/tools/save-editor/Ops.lua index 4c6ebfef..b0448f97 100644 --- a/tools/save-editor/Ops.lua +++ b/tools/save-editor/Ops.lua @@ -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) diff --git a/tools/save-editor/panels/MonEditor.lua b/tools/save-editor/panels/MonEditor.lua index dbaf9699..1e06e20c 100644 --- a/tools/save-editor/panels/MonEditor.lua +++ b/tools/save-editor/panels/MonEditor.lua @@ -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)