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)