Bläddra i källkod

Merge branch 'feature/data' into develop

Iñigo Valentin 10 timmar sedan
förälder
incheckning
69c3de913f
62 ändrade filer med 5083 tillägg och 994 borttagningar
  1. 5 0
      data/data/config/config.cfg
  2. 6 1
      data/data/screens/_screens.xml
  3. 298 0
      data/data/screens/config_menu/config_menu.xml
  4. 0 22
      data/data/screens/item_menu/prototypes.xml
  5. 83 0
      data/data/screens/limit_menu/limit_menu.xml
  6. 149 0
      data/data/screens/magic_menu/magic_menu.xml
  7. 2 2
      data/data/screens/main_menu/main_menu.xml
  8. 99 0
      data/data/screens/phs_menu/phs_menu.xml
  9. 11 0
      data/data/screens/prototypes.xml
  10. 15 0
      data/data/screens/quit_menu/quit_menu.xml
  11. 241 0
      data/data/screens/status_menu/status_menu.xml
  12. 3 0
      data/data/scripts/_scripts.xml
  13. 25 0
      data/data/scripts/battle.lua
  14. 44 11
      data/data/scripts/battle/battle_ui.lua
  15. 159 0
      data/data/scripts/character_abilities.lua
  16. 165 0
      data/data/scripts/character_effects.lua
  17. 2 33
      data/data/scripts/config.lua
  18. 2 1
      data/data/scripts/debug_data.lua
  19. 5 0
      data/data/scripts/field.lua
  20. 136 125
      data/data/scripts/menu/begin_menu.lua
  21. 687 0
      data/data/scripts/menu/config_menu.lua
  22. 20 13
      data/data/scripts/menu/equip_menu.lua
  23. 24 31
      data/data/scripts/menu/item_menu.lua
  24. 235 0
      data/data/scripts/menu/limit_menu.lua
  25. 393 0
      data/data/scripts/menu/magic_menu.lua
  26. 182 161
      data/data/scripts/menu/main_menu.lua
  27. 1 0
      data/data/scripts/menu/materia_menu.lua
  28. 20 33
      data/data/scripts/menu/menu_helpers.lua
  29. 467 0
      data/data/scripts/menu/menu_lib.lua
  30. 47 18
      data/data/scripts/menu/name_menu.lua
  31. 31 44
      data/data/scripts/menu/pause_menu.lua
  32. 313 0
      data/data/scripts/menu/phs_menu.lua
  33. 49 0
      data/data/scripts/menu/quit_menu.lua
  34. 43 97
      data/data/scripts/menu/save_menu.lua
  35. 215 0
      data/data/scripts/menu/status_menu.lua
  36. 124 148
      data/data/scripts/save.lua
  37. 38 37
      data/data/scripts/system.lua
  38. 114 2
      data/data/texts/english/menu.xml
  39. 23 0
      src/core/AudioManager.cpp
  40. 35 0
      src/core/AudioManager.h
  41. 13 8
      src/core/EntityManager.cpp
  42. 16 0
      src/core/EntityManager.h
  43. 88 0
      src/core/InputManager.cpp
  44. 76 0
      src/core/InputManager.h
  45. 20 0
      src/core/InputManagerCommands.h
  46. 85 48
      src/core/Savemap.cpp
  47. 23 112
      src/core/Savemap.h
  48. 14 8
      src/core/SavemapHandler.cpp
  49. 19 11
      src/core/SavemapHandler.h
  50. 4 11
      src/core/ScriptManager.cpp
  51. 21 3
      src/core/ScriptManagerBinds.h
  52. 25 0
      src/core/UiManager.cpp
  53. 45 0
      src/core/UiManager.h
  54. 4 0
      src/core/UiWidget.cpp
  55. 22 0
      src/core/UiWidget.h
  56. 4 0
      src/core/XmlScreenFile.cpp
  57. 35 5
      src/installer/KernelDataInstaller.cpp
  58. 2 0
      src/installer/KernelDataInstaller.h
  59. 25 3
      src/installer/decompiler/field/instruction/FieldPartyInstruction.cpp
  60. 15 0
      src/installer/decompiler/field/instruction/FieldPartyInstruction.h
  61. 9 0
      src/main.cpp
  62. 7 6
      src/modules/worldmap/WorldmapModule.cpp

+ 5 - 0
data/data/config/config.cfg

@@ -49,6 +49,11 @@ bind_game_event V "SELECT"
 bind_game_event PGDN "R1"
 bind_game_event PGUP "L1"
 
+bind_game_event Up "Up"
+bind_game_event Down "Down"
+bind_game_event Left "Left"
+bind_game_event Right "Right"
+
 bind_game_event LCtrl "run"
 bind_game_event Up "walk_up"
 bind_game_event Right "walk_right"

+ 6 - 1
data/data/screens/_screens.xml

@@ -7,10 +7,15 @@
     <prototype file_name="screens/main_menu/prototypes.xml" />
     <screen file_name="screens/main_menu/main_menu.xml" />
     <screen file_name="screens/pause_menu/pause_menu.xml" />
-    <prototype file_name="screens/item_menu/prototypes.xml" />
     <screen file_name="screens/item_menu/item_menu.xml" />
     <screen file_name="screens/equip_menu/equip_menu.xml" />
     <screen file_name="screens/materia_menu/materia_menu.xml" />
+    <screen file_name="screens/status_menu/status_menu.xml" />
+    <screen file_name="screens/magic_menu/magic_menu.xml" />
+    <screen file_name="screens/limit_menu/limit_menu.xml" />
+    <screen file_name="screens/quit_menu/quit_menu.xml" />
+    <screen file_name="screens/phs_menu/phs_menu.xml" />
+    <screen file_name="screens/config_menu/config_menu.xml" />
     <screen file_name="screens/name_menu/name_menu.xml" />
     <screen file_name="screens/save_menu/save_menu.xml" />
     <prototype file_name="screens/save_menu/prototypes.xml" />

+ 298 - 0
data/data/screens/config_menu/config_menu.xml

@@ -0,0 +1,298 @@
+<screen name="ConfigMenu" script="scripts/menu/config_menu.lua" visible="false">
+    <widget name="Container" width="40%" height="40%" x="0" y="0" align="left" valign="top" scale="2.5 2.5" visible="true">
+        <widget name="Help" x="0" y="0" width="100%-97" height="26" visible="true">
+            <prototype name="Window" />
+            <text_area name="H1" text_name="ConfigMenuHelp1" x="16" y="7" font="FFVIIFont" visible="true" />
+            <text_area name="H2" text_name="ConfigMenuHelp2" x="16" y="7" font="FFVIIFont" visible="false" />
+            <text_area name="H3" text_name="ConfigMenuHelp3" x="16" y="7" font="FFVIIFont" visible="false" />
+            <text_area name="H4" text_name="ConfigMenuHelp4" x="16" y="7" font="FFVIIFont" visible="false" />
+            <text_area name="H5" text_name="ConfigMenuHelp5" x="16" y="7" font="FFVIIFont" visible="false" />
+            <text_area name="H6" text_name="ConfigMenuHelp6" x="16" y="7" font="FFVIIFont" visible="false" />
+            <text_area name="H7" text_name="ConfigMenuHelp7" x="16" y="7" font="FFVIIFont" visible="false" />
+            <text_area name="H8" text_name="ConfigMenuHelp8" x="16" y="7" font="FFVIIFont" visible="false" />
+            <text_area name="H9" text_name="ConfigMenuHelp9" x="16" y="7" font="FFVIIFont" visible="false" />
+            <text_area name="H10" text_name="ConfigMenuHelp10" x="16" y="7" font="FFVIIFont" visible="false" />
+            <text_area name="H11" text_name="ConfigMenuHelp11" x="16" y="7" font="FFVIIFont" visible="false" />
+            <text_area name="H12" text_name="ConfigMenuHelp12" x="16" y="7" font="FFVIIFont" visible="false" />
+        </widget>
+        <widget name="Title" x="100%-97" y="0" width="97" height="26" visible="true">
+            <prototype name="Window" />
+            <text_area name="Title" text_name="ConfigMenuTitle" x="16" y="7" font="FFVIIFont" visible="true" />
+        </widget>
+        <widget name="Body" x="0" y="26" width="100%" height="100%-26" visible="true">
+            <prototype name="Window" />
+            <widget name="WindowColor" x="0" y="18" width="100%" height="16" visible="true">
+                <text_area name="Label" text_name="ConfigMenuLabelWindowColor" x="29" y="0" font="FFVIIFont" visible="true" />
+                <widget name="Preview" x="170" y="-12" width="67" height="26" visible="true">
+                    <prototype name="WindowPreview" />
+                </widget>
+            </widget>
+            <widget name="Sound" x="0" y="39" width="100%" height="16" visible="true">
+                <text_area name="Label" text_name="ConfigMenuLabelSound" x="29" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="Controller" x="0" y="60" width="100%" height="16" visible="true">
+                <text_area name="Label" text_name="ConfigMenuLabelController" x="29" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="CursorMode" x="0" y="81" width="100%" height="16" visible="true">
+                <text_area name="Label" text_name="ConfigMenuLabelCursor" x="29" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Opt1On" text_name="ConfigMenuInitial" x="167" y="0" font="FFVIIFont" visible="false" />
+                <text_area name="Opt1Off" text_name="ConfigMenuInitialOff" x="167" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Opt2On" text_name="ConfigMenuMemory" x="240" y="0" font="FFVIIFont" visible="false" />
+                <text_area name="Opt2Off" text_name="ConfigMenuMemoryOff" x="240" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="Atb" x="0" y="102" width="100%" height="16" visible="true">
+                <text_area name="Label" text_name="ConfigMenuLabelAtb" x="29" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Opt1On" text_name="ConfigMenuActive" x="167" y="0" font="FFVIIFont" visible="false" />
+                <text_area name="Opt1Off" text_name="ConfigMenuActiveOff" x="167" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Opt2On" text_name="ConfigMenuRecommended" x="220" y="0" font="FFVIIFont" visible="false" />
+                <text_area name="Opt2Off" text_name="ConfigMenuRecommendedOff" x="220" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Opt3On" text_name="ConfigMenuWait" x="326" y="0" font="FFVIIFont" visible="false" />
+                <text_area name="Opt3Off" text_name="ConfigMenuWaitOff" x="326" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="BattleSpeed" x="0" y="123" width="100%" height="16" visible="true">
+                <text_area name="Label" text_name="ConfigMenuLabelBattleSpeed" x="29" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="FastLbl" text_name="ConfigMenuFast" x="158" y="0" font="FFVIIFont" visible="true" />
+                <sprite name="Bar" x="190" y="2" width="150" height="10" colour="0.12 0.12 0.3" visible="true" />
+                <sprite name="Handle" x="190" y="0" width="8" height="14" colour="0.75 0.75 0.75" visible="true">
+                    <animation name="Position0" length="0" x="0:190" />
+                    <animation name="Position1" length="0" x="0:204" />
+                    <animation name="Position2" length="0" x="0:218" />
+                    <animation name="Position3" length="0" x="0:233" />
+                    <animation name="Position4" length="0" x="0:247" />
+                    <animation name="Position5" length="0" x="0:261" />
+                    <animation name="Position6" length="0" x="0:275" />
+                    <animation name="Position7" length="0" x="0:289" />
+                    <animation name="Position8" length="0" x="0:304" />
+                    <animation name="Position9" length="0" x="0:318" />
+                    <animation name="Position10" length="0" x="0:332" />
+                </sprite>
+                <text_area name="SlowLbl" text_name="ConfigMenuSlow" x="343" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="BattleMessage" x="0" y="144" width="100%" height="16" visible="true">
+                <text_area name="Label" text_name="ConfigMenuLabelBattleMessage" x="29" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="FastLbl" text_name="ConfigMenuFast" x="158" y="0" font="FFVIIFont" visible="true" />
+                <sprite name="Bar" x="190" y="2" width="150" height="10" colour="0.12 0.12 0.3" visible="true" />
+                <sprite name="Handle" x="190" y="0" width="8" height="14" colour="0.75 0.75 0.75" visible="true">
+                    <animation name="Position0" length="0" x="0:190" />
+                    <animation name="Position1" length="0" x="0:204" />
+                    <animation name="Position2" length="0" x="0:218" />
+                    <animation name="Position3" length="0" x="0:233" />
+                    <animation name="Position4" length="0" x="0:247" />
+                    <animation name="Position5" length="0" x="0:261" />
+                    <animation name="Position6" length="0" x="0:275" />
+                    <animation name="Position7" length="0" x="0:289" />
+                    <animation name="Position8" length="0" x="0:304" />
+                    <animation name="Position9" length="0" x="0:318" />
+                    <animation name="Position10" length="0" x="0:332" />
+                </sprite>
+                <text_area name="SlowLbl" text_name="ConfigMenuSlow" x="343" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="FieldMessage" x="0" y="165" width="100%" height="16" visible="true">
+                <text_area name="Label" text_name="ConfigMenuLabelFieldMessage" x="29" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="FastLbl" text_name="ConfigMenuFast" x="158" y="0" font="FFVIIFont" visible="true" />
+                <sprite name="Bar" x="190" y="2" width="150" height="10" colour="0.12 0.12 0.3" visible="true" />
+                <sprite name="Handle" x="190" y="0" width="8" height="14" colour="0.75 0.75 0.75" visible="true">
+                    <animation name="Position0" length="0" x="0:190" />
+                    <animation name="Position1" length="0" x="0:204" />
+                    <animation name="Position2" length="0" x="0:218" />
+                    <animation name="Position3" length="0" x="0:233" />
+                    <animation name="Position4" length="0" x="0:247" />
+                    <animation name="Position5" length="0" x="0:261" />
+                    <animation name="Position6" length="0" x="0:275" />
+                    <animation name="Position7" length="0" x="0:289" />
+                    <animation name="Position8" length="0" x="0:304" />
+                    <animation name="Position9" length="0" x="0:318" />
+                    <animation name="Position10" length="0" x="0:332" />
+                </sprite>
+                <text_area name="SlowLbl" text_name="ConfigMenuSlow" x="343" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="Movement" x="0" y="186" width="100%" height="16" visible="true">
+                <text_area name="Label" text_name="ConfigMenuLabelMovement" x="29" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Opt1On" text_name="ConfigMenuWalk" x="167" y="0" font="FFVIIFont" visible="false" />
+                <text_area name="Opt1Off" text_name="ConfigMenuWalkOff" x="167" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Opt2On" text_name="ConfigMenuRun" x="240" y="0" font="FFVIIFont" visible="false" />
+                <text_area name="Opt2Off" text_name="ConfigMenuRunOff" x="240" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="CameraAngle" x="0" y="207" width="100%" height="16" visible="true">
+                <text_area name="Label" text_name="ConfigMenuLabelCameraAngle" x="29" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Opt1On" text_name="ConfigMenuAuto" x="167" y="0" font="FFVIIFont" visible="false" />
+                <text_area name="Opt1Off" text_name="ConfigMenuAutoOff" x="167" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Opt2On" text_name="ConfigMenuFix" x="240" y="0" font="FFVIIFont" visible="false" />
+                <text_area name="Opt2Off" text_name="ConfigMenuFixOff" x="240" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="MagicOrder" x="0" y="228" width="100%" height="16" visible="true">
+                <text_area name="Label" text_name="ConfigMenuLabelMagicOrder" x="29" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="No" text_name="ConfigMenuMagicNo" x="150" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Number" x="180" y="2" font="FFVIIMenuDigits" visible="true" />
+                <text_area name="Order1" text_name="ConfigMenuMagicOrder1" x="195" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Order2" text_name="ConfigMenuMagicOrder2" x="195" y="0" font="FFVIIFont" visible="false" />
+                <text_area name="Order3" text_name="ConfigMenuMagicOrder3" x="195" y="0" font="FFVIIFont" visible="false" />
+                <text_area name="Order4" text_name="ConfigMenuMagicOrder4" x="195" y="0" font="FFVIIFont" visible="false" />
+                <text_area name="Order5" text_name="ConfigMenuMagicOrder5" x="195" y="0" font="FFVIIFont" visible="false" />
+                <text_area name="Order6" text_name="ConfigMenuMagicOrder6" x="195" y="0" font="FFVIIFont" visible="false" />
+            </widget>
+        </widget>
+        <sprite name="CornerCursor" image="icons/cursor.png" x="146" y="28" width="24" height="17" visible="false">
+            <animation name="Position1" length="0" x="0:146" y="0:33" />
+            <animation name="Position2" length="0" x="0:210" y="0:33" />
+            <animation name="Position3" length="0" x="0:146" y="0:52" />
+            <animation name="Position4" length="0" x="0:210" y="0:52" />
+        </sprite>
+        <widget name="ColourWindow" x="130" y="68" width="210" height="88" visible="false">
+            <prototype name="Window" />
+            <widget name="Red" x="0" y="12" width="100%" height="16" visible="true">
+                <text_area name="Label" text_name="ConfigMenuColourRed" x="26" y="0" font="FFVIIFont" visible="true" />
+                <sprite name="Bar" x="70" y="3" width="100" height="8" colour="0.55 0.12 0.12" visible="true" />
+                <sprite name="Handle" x="70" y="0" width="8" height="14" colour="0.85 0.85 0.85" visible="true" />
+                <text_area name="Value" x="178" y="2" font="FFVIIMenuDigits" visible="true" />
+            </widget>
+            <widget name="Green" x="0" y="36" width="100%" height="16" visible="true">
+                <text_area name="Label" text_name="ConfigMenuColourGreen" x="26" y="0" font="FFVIIFont" visible="true" />
+                <sprite name="Bar" x="70" y="3" width="100" height="8" colour="0.12 0.45 0.12" visible="true" />
+                <sprite name="Handle" x="70" y="0" width="8" height="14" colour="0.85 0.85 0.85" visible="true" />
+                <text_area name="Value" x="178" y="2" font="FFVIIMenuDigits" visible="true" />
+            </widget>
+            <widget name="Blue" x="0" y="60" width="100%" height="16" visible="true">
+                <text_area name="Label" text_name="ConfigMenuColourBlue" x="26" y="0" font="FFVIIFont" visible="true" />
+                <sprite name="Bar" x="70" y="3" width="100" height="8" colour="0.15 0.15 0.65" visible="true" />
+                <sprite name="Handle" x="70" y="0" width="8" height="14" colour="0.85 0.85 0.85" visible="true" />
+                <text_area name="Value" x="178" y="2" font="FFVIIMenuDigits" visible="true" />
+            </widget>
+            <sprite name="Cursor" image="icons/cursor.png" x="0" y="9" width="24" height="17" visible="true">
+                <animation name="Position1" length="0" y="0:9" />
+                <animation name="Position2" length="0" y="0:33" />
+                <animation name="Position3" length="0" y="0:57" />
+            </sprite>
+        </widget>
+        <widget name="SoundWindow" x="130" y="66" width="210" height="64" visible="false">
+            <prototype name="Window" />
+            <widget name="Music" x="0" y="12" width="100%" height="16" visible="true">
+                <text_area name="Label" text_name="ConfigMenuSoundMusic" x="26" y="0" font="FFVIIFont" visible="true" />
+                <sprite name="Bar" x="80" y="3" width="100" height="8" colour="0.15 0.15 0.45" visible="true" />
+                <sprite name="Handle" x="80" y="0" width="8" height="14" colour="0.85 0.85 0.85" visible="true">
+                    <animation name="Position0" length="0" x="0:80" />
+                    <animation name="Position1" length="0" x="0:103" />
+                    <animation name="Position2" length="0" x="0:126" />
+                    <animation name="Position3" length="0" x="0:149" />
+                    <animation name="Position4" length="0" x="0:172" />
+                </sprite>
+            </widget>
+            <widget name="Effects" x="0" y="36" width="100%" height="16" visible="true">
+                <text_area name="Label" text_name="ConfigMenuSoundEffects" x="26" y="0" font="FFVIIFont" visible="true" />
+                <sprite name="Bar" x="80" y="3" width="100" height="8" colour="0.15 0.15 0.45" visible="true" />
+                <sprite name="Handle" x="80" y="0" width="8" height="14" colour="0.85 0.85 0.85" visible="true">
+                    <animation name="Position0" length="0" x="0:80" />
+                    <animation name="Position1" length="0" x="0:103" />
+                    <animation name="Position2" length="0" x="0:126" />
+                    <animation name="Position3" length="0" x="0:149" />
+                    <animation name="Position4" length="0" x="0:172" />
+                </sprite>
+            </widget>
+            <sprite name="Cursor" image="icons/cursor.png" x="0" y="9" width="24" height="17" visible="true">
+                <animation name="Position1" length="0" y="0:9" />
+                <animation name="Position2" length="0" y="0:33" />
+            </sprite>
+        </widget>
+        <widget name="ControllerWindow" x="60" y="36" width="290" height="250" visible="false">
+            <prototype name="Window" />
+            <widget name="RowDefault" x="0" y="10" width="100%" height="14" visible="true">
+                <text_area name="Label" text_name="ConfigMenuControlDefault" x="26" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="RowUp" x="0" y="26" width="100%" height="14" visible="true">
+                <text_area name="Label" text_name="ConfigMenuControlUp" x="26" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Keys" x="150" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="RowDown" x="0" y="42" width="100%" height="14" visible="true">
+                <text_area name="Label" text_name="ConfigMenuControlDown" x="26" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Keys" x="150" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="RowLeft" x="0" y="58" width="100%" height="14" visible="true">
+                <text_area name="Label" text_name="ConfigMenuControlLeft" x="26" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Keys" x="150" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="RowRight" x="0" y="74" width="100%" height="14" visible="true">
+                <text_area name="Label" text_name="ConfigMenuControlRight" x="26" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Keys" x="150" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="RowOk" x="0" y="90" width="100%" height="14" visible="true">
+                <text_area name="Label" text_name="ConfigMenuControlOk" x="26" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Keys" x="150" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="RowCancel" x="0" y="106" width="100%" height="14" visible="true">
+                <text_area name="Label" text_name="ConfigMenuControlCancel" x="26" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Keys" x="150" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="RowMenu" x="0" y="122" width="100%" height="14" visible="true">
+                <text_area name="Label" text_name="ConfigMenuControlMenu" x="26" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Keys" x="150" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="RowExtra" x="0" y="138" width="100%" height="14" visible="true">
+                <text_area name="Label" text_name="ConfigMenuControlExtra" x="26" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Keys" x="150" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="RowSelect" x="0" y="154" width="100%" height="14" visible="true">
+                <text_area name="Label" text_name="ConfigMenuControlSelect" x="26" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Keys" x="150" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="RowStart" x="0" y="170" width="100%" height="14" visible="true">
+                <text_area name="Label" text_name="ConfigMenuControlStart" x="26" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Keys" x="150" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="RowL1" x="0" y="186" width="100%" height="14" visible="true">
+                <text_area name="Label" text_name="ConfigMenuControlL1" x="26" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Keys" x="150" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="RowR1" x="0" y="202" width="100%" height="14" visible="true">
+                <text_area name="Label" text_name="ConfigMenuControlR1" x="26" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Keys" x="150" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="RowRun" x="0" y="218" width="100%" height="14" visible="true">
+                <text_area name="Label" text_name="ConfigMenuControlRun" x="26" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Keys" x="150" y="0" font="FFVIIFont" visible="true" />
+            </widget>
+            <sprite name="Cursor" image="icons/cursor.png" x="0" y="7" width="24" height="17" visible="true">
+                <animation name="Position1" length="0" y="0:7" />
+                <animation name="Position2" length="0" y="0:23" />
+                <animation name="Position3" length="0" y="0:39" />
+                <animation name="Position4" length="0" y="0:55" />
+                <animation name="Position5" length="0" y="0:71" />
+                <animation name="Position6" length="0" y="0:87" />
+                <animation name="Position7" length="0" y="0:103" />
+                <animation name="Position8" length="0" y="0:119" />
+                <animation name="Position9" length="0" y="0:135" />
+                <animation name="Position10" length="0" y="0:151" />
+                <animation name="Position11" length="0" y="0:167" />
+                <animation name="Position12" length="0" y="0:183" />
+                <animation name="Position13" length="0" y="0:199" />
+                <animation name="Position14" length="0" y="0:215" />
+                <animation name="Blink1" length="0.1" y="0:7" alpha="0:1, 0.04:1, 0.05:0, 0.09:0, 0.1:1" />
+                <animation name="Blink2" length="0.1" y="0:23" alpha="0:1, 0.04:1, 0.05:0, 0.09:0, 0.1:1" />
+                <animation name="Blink3" length="0.1" y="0:39" alpha="0:1, 0.04:1, 0.05:0, 0.09:0, 0.1:1" />
+                <animation name="Blink4" length="0.1" y="0:55" alpha="0:1, 0.04:1, 0.05:0, 0.09:0, 0.1:1" />
+                <animation name="Blink5" length="0.1" y="0:71" alpha="0:1, 0.04:1, 0.05:0, 0.09:0, 0.1:1" />
+                <animation name="Blink6" length="0.1" y="0:87" alpha="0:1, 0.04:1, 0.05:0, 0.09:0, 0.1:1" />
+                <animation name="Blink7" length="0.1" y="0:103" alpha="0:1, 0.04:1, 0.05:0, 0.09:0, 0.1:1" />
+                <animation name="Blink8" length="0.1" y="0:119" alpha="0:1, 0.04:1, 0.05:0, 0.09:0, 0.1:1" />
+                <animation name="Blink9" length="0.1" y="0:135" alpha="0:1, 0.04:1, 0.05:0, 0.09:0, 0.1:1" />
+                <animation name="Blink10" length="0.1" y="0:151" alpha="0:1, 0.04:1, 0.05:0, 0.09:0, 0.1:1" />
+                <animation name="Blink11" length="0.1" y="0:167" alpha="0:1, 0.04:1, 0.05:0, 0.09:0, 0.1:1" />
+                <animation name="Blink12" length="0.1" y="0:183" alpha="0:1, 0.04:1, 0.05:0, 0.09:0, 0.1:1" />
+                <animation name="Blink13" length="0.1" y="0:199" alpha="0:1, 0.04:1, 0.05:0, 0.09:0, 0.1:1" />
+                <animation name="Blink14" length="0.1" y="0:215" alpha="0:1, 0.04:1, 0.05:0, 0.09:0, 0.1:1" />
+            </sprite>
+        </widget>
+        <sprite name="Cursor" image="icons/cursor.png" x="-1" y="46" width="24" height="17" visible="true">
+            <animation name="Position1" length="0" y="0:46" />
+            <animation name="Position2" length="0" y="0:66" />
+            <animation name="Position3" length="0" y="0:88" />
+            <animation name="Position4" length="0" y="0:108" />
+            <animation name="Position5" length="0" y="0:130" />
+            <animation name="Position6" length="0" y="0:150" />
+            <animation name="Position7" length="0" y="0:172" />
+            <animation name="Position8" length="0" y="0:192" />
+            <animation name="Position9" length="0" y="0:214" />
+            <animation name="Position10" length="0" y="0:234" />
+            <animation name="Position11" length="0" y="0:256" />
+        </sprite>
+    </widget>
+</screen>

+ 0 - 22
data/data/screens/item_menu/prototypes.xml

@@ -1,22 +0,0 @@
-<prototypes>
-    <prototype name="MenuCharacterData">
-        <text_area name="Name" x="0" y="0" text_name="CharacterDataName" font="FFVIIFont" visible="true" />
-        <text_area name="Lv" x="0" y="5" text_name="CharacterDataLv" colour="0 0.9 0.9" font="FFVIIMenuFont" visible="true" />
-        <text_area name="LvNumber" x="18" y="13" text_name="CharacterDataLvNumber" font="FFVIIMenuDigits" visible="true" />
-        <text_area name="Hp" x="0" y="16" text_name="CharacterDataHp" colour="0 0.9 0.9" font="FFVIIMenuFont" visible="true" />
-        <text_area name="HpCurrent" x="18" y="23" text_name="CharacterDataHpCurrent" colour="0.9 0.9 0.9" font="FFVIIMenuDigits" visible="true" />
-        <sprite name="HpSlash" image="screens/images/slash.png" x="46" y="23" width="4" height="8" visible="true" />
-        <text_area name="HpMax" x="50" y="23" text_name="CharacterDataHpMax" font="FFVIIMenuDigits" visible="true" />
-        <sprite name="HpLine" x="18" y="31" width="60" height="1" colour="0.3 0 0" visible="true" />
-        <sprite name="HpLineCurrent" x="18" y="31" width="5" height="1" colours="0 0.5 1, 0.78 0.78 0.78, 0.78 0.78 0.78, 0 0.5 1" visible="true" />
-        <sprite name="HpLineShadow" x="18" y="32" width="60" height="1" colour="0 0 0" visible="true" />
-        <text_area name="Mp" x="0" y="26" text_name="CharacterDataMp" colour="0 0.9 0.9" font="FFVIIMenuFont" visible="true" />
-        <text_area name="MpCurrent" x="18" y="33" text_name="CharacterDataMpCurrent" font="FFVIIMenuDigits" visible="true" />
-        <sprite name="MpSlash" image="icons/slash.png" x="46" y="33" width="4" height="8" visible="true" />
-        <text_area name="MpMax" x="50" y="33" text_name="CharacterDataMpMax" font="FFVIIMenuDigits" visible="true" />
-        <sprite name="MpLine" x="18" y="41" width="60" height="1" colour="0.3 0 0" visible="true" />
-        <sprite name="MpLineCurrent" x="18" y="41" width="60" height="1" colours="0 1 0.5, 0.78 0.78 0.78, 0.78 0.78 0.78, 0 1 0.5" visible="true" />
-        <sprite name="MpLineShadow" x="18" y="42" width="60" height="1" colour="0 0 0" visible="true" />
-        <text_area name="Status" x="36" y="11" text_name="CharacterDataFury" colour="0.9 0 0.9" font="FFVIIFont" visible="true" />
-    </prototype>
-</prototypes>

+ 83 - 0
data/data/screens/limit_menu/limit_menu.xml

@@ -0,0 +1,83 @@
+<screen name="LimitMenu" script="scripts/menu/limit_menu.lua" visible="false">
+    <widget name="Container" width="40%" height="40%" x="0" y="0" align="left" valign="top" scale="2.5 2.5" visible="true">
+        <widget name="Character" width="100%" height="70" visible="true">
+            <prototype name="Window" />
+            <sprite name="Portrait" image="other/choco.png" x="10" y="12" width="42" height="48" visible="true" />
+            <widget name="Data" x="66" y="14" width="100%-66" height="100%-14" visible="true">
+                <prototype name="MenuCharacterData" />
+            </widget>
+            <text_area name="LimitLbl" text_name="LimitMenuLimitLevel" x="181" y="12" font="FFVIIFont" visible="true" />
+            <text_area name="LimitLevel" x="242" y="14" font="FFVIIMenuDigits" visible="true" />
+            <sprite name="LimitLevelBar" image="icons/menu_bar.png" x="196" y="26" width="56" height="8" visible="true" />
+            <sprite name="LimitLinePart1" x="197" y="27" width="25" height="3" colours="0.5 0.5 0.5, 0.5 0.5 0.5, 0.5 0.125 0.31, 0.5 0.125 0.31" alpha="0.8" visible="false" />
+            <sprite name="LimitLinePart2" x="197" y="30" width="25" height="3" colours="0.5 0.125 0.31, 0.5 0.125 0.31, 0 0 0, 0 0 0" alpha="0.8" visible="false" />
+            <widget name="Modes" x="0" y="44" width="100%" height="14" visible="true">
+                <text_area name="Set" text_name="LimitMenuSet" x="197" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Check" text_name="LimitMenuCheck" x="245" y="0" font="FFVIIFont" visible="true" />
+                <sprite name="Cursor" image="icons/cursor.png" x="170" y="1" width="24" height="17" visible="true">
+                    <animation name="Position1" length="0" x="0:170" />
+                    <animation name="Position2" length="0" x="0:218" />
+                </sprite>
+            </widget>
+        </widget>
+        <widget name="Title" x="100%-70" y="0" width="70" height="22" visible="true">
+            <prototype name="Window" />
+            <text_area name="Title" text_name="LimitMenuTitle" x="12" y="5" font="FFVIIFont" visible="true" />
+        </widget>
+        <widget name="Help" x="0" y="70" width="100%" height="24" visible="true">
+            <prototype name="Window" />
+            <text_area name="Help" x="16" y="6" font="FFVIIFont" visible="true" />
+        </widget>
+        <widget name="Body" x="0" y="94" width="100%" height="100%-94" visible="true">
+            <prototype name="Window" />
+            <widget name="Level1" x="28" y="30" width="120" height="40" visible="true">
+                <text_area name="Lbl" x="0" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Tech1" x="11" y="13" font="FFVIIFont" visible="true" />
+                <text_area name="Tech2" x="11" y="26" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="Level2" x="200" y="30" width="120" height="40" visible="true">
+                <text_area name="Lbl" x="0" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Tech1" x="11" y="13" font="FFVIIFont" visible="true" />
+                <text_area name="Tech2" x="11" y="26" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="Level3" x="28" y="100" width="120" height="40" visible="true">
+                <text_area name="Lbl" x="0" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Tech1" x="11" y="13" font="FFVIIFont" visible="true" />
+                <text_area name="Tech2" x="11" y="26" font="FFVIIFont" visible="true" />
+            </widget>
+            <widget name="Level4" x="200" y="100" width="120" height="40" visible="true">
+                <text_area name="Lbl" x="0" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Tech1" x="11" y="13" font="FFVIIFont" visible="true" />
+                <text_area name="Tech2" x="11" y="26" font="FFVIIFont" visible="true" />
+            </widget>
+            <sprite name="LevelCursor" image="icons/cursor.png" x="-2" y="28" width="24" height="17" visible="false">
+                <animation name="Position1" length="0" x="0:-2" y="0:28" />
+                <animation name="Position2" length="0" x="0:170" y="0:28" />
+                <animation name="Position3" length="0" x="0:-2" y="0:98" />
+                <animation name="Position4" length="0" x="0:170" y="0:98" />
+            </sprite>
+            <sprite name="TechCursor" image="icons/cursor.png" x="9" y="40" width="24" height="17" visible="false">
+                <animation name="Position1" length="0" x="0:9" y="0:40" />
+                <animation name="Position2" length="0" x="0:9" y="0:53" />
+                <animation name="Position3" length="0" x="0:181" y="0:40" />
+                <animation name="Position4" length="0" x="0:181" y="0:53" />
+                <animation name="Position5" length="0" x="0:9" y="0:110" />
+                <animation name="Position6" length="0" x="0:9" y="0:123" />
+                <animation name="Position7" length="0" x="0:181" y="0:110" />
+                <animation name="Position8" length="0" x="0:181" y="0:123" />
+            </sprite>
+        </widget>
+        <widget name="Confirm" x="78" y="46" width="164" height="82" visible="false">
+            <prototype name="Window" />
+            <text_area name="Line1" text_name="LimitMenuConfirm1" x="8" y="7" font="FFVIIFont" visible="true" />
+            <text_area name="Line2" text_name="LimitMenuConfirm2" x="8" y="21" font="FFVIIFont" visible="true" />
+            <text_area name="Line3" text_name="LimitMenuConfirm3" x="8" y="35" font="FFVIIFont" visible="true" />
+            <text_area name="Yes" text_name="Yes" x="70" y="54" font="FFVIIFont" visible="true" />
+            <text_area name="No" text_name="No" x="70" y="66" font="FFVIIFont" visible="true" />
+            <sprite name="Cursor" image="icons/cursor.png" x="44" y="55" width="24" height="17" visible="true">
+                <animation name="Position1" length="0" y="0:43" />
+                <animation name="Position2" length="0" y="0:55" />
+            </sprite>
+        </widget>
+    </widget>
+</screen>

+ 149 - 0
data/data/screens/magic_menu/magic_menu.xml

@@ -0,0 +1,149 @@
+<screen name="MagicMenu" script="scripts/menu/magic_menu.lua" visible="false">
+    <widget name="Container" width="40%" height="40%" x="0" y="0" align="left" valign="top" scale="2.5 2.5" visible="true">
+        <widget name="Character" x="0" y="0" width="100%-98" height="94" visible="true">
+            <prototype name="Window" />
+            <sprite name="Portrait" image="other/choco.png" x="11" y="20" width="53" height="58" visible="true" />
+            <widget name="Data" x="68" y="28" width="100%-68" height="100%-28" visible="true">
+                <prototype name="MenuCharacterData" />
+            </widget>
+            <widget name="Added" x="158" y="14" width="150" height="40" visible="false">
+                <text_area name="Label" text_name="MagicMenuAddedAbility" x="0" y="0" font="FFVIIFont" visible="true" />
+                <text_area name="Value" text_name="MagicMenuNothing" x="13" y="20" font="FFVIIFont" visible="true" />
+            </widget>
+        </widget>
+        <widget name="Title" x="100%-98" y="0" width="98" height="27" visible="true">
+            <prototype name="Window" />
+            <text_area name="Title" text_name="MagicMenuTitle" x="24" y="7" font="FFVIIFont" visible="true" />
+        </widget>
+        <widget name="Side" x="100%-98" y="27" width="98" height="67" visible="true">
+            <prototype name="Window" />
+            <widget name="Options" x="0" y="0" width="100%" height="100%" visible="true">
+                <text_area name="Magic" text_name="MagicMenuMagic" x="34" y="8" font="FFVIIFont" visible="true" />
+                <text_area name="Summon" text_name="MagicMenuSummon" x="34" y="25" font="FFVIIFont" visible="true" />
+                <text_area name="ESkill" text_name="MagicMenuESkill" x="34" y="42" font="FFVIIFont" visible="true" />
+                <sprite name="Cursor" image="icons/cursor.png" x="8" y="6" width="24" height="17" visible="true">
+                    <animation name="Position1" length="0" y="0:6" />
+                    <animation name="Position2" length="0" y="0:23" />
+                    <animation name="Position3" length="0" y="0:40" />
+                </sprite>
+            </widget>
+            <widget name="Cost" x="0" y="0" width="100%" height="100%" visible="false">
+                <text_area name="MpLabel" text_name="MagicMenuMpNeeded" x="14" y="6" font="FFVIIFont" visible="true" />
+                <text_area name="MpValue" x="40" y="20" font="FFVIIMenuDigits" visible="true" />
+                <widget name="Uses" x="0" y="0" width="100%" height="100%" visible="false">
+                    <text_area name="Label" text_name="MagicMenuUses" x="30" y="34" font="FFVIIFont" visible="true" />
+                    <text_area name="Value" x="40" y="48" font="FFVIIMenuDigits" visible="true" />
+                </widget>
+            </widget>
+        </widget>
+        <widget name="Help" x="0" y="94" width="100%" height="26" visible="true">
+            <prototype name="Window" />
+            <text_area name="Help" x="16" y="7" font="FFVIIFont" visible="true" />
+        </widget>
+        <widget name="List" x="0" y="120" width="100%" height="100%-120" visible="true">
+            <prototype name="Window" />
+            <sprite name="ScrollBg" x="100%-12" y="2" width="10" height="100%-6" colour="0.2 0.2 0.2" alpha="0.8" visible="true" />
+            <sprite name="Scroll" x="100%-11" y="3" width="8" height="18" colour="0.7 0.7 0.7" visible="true" />
+            <text_area name="R1C1" x="24" y="6" font="FFVIIFont" visible="true" />
+            <text_area name="R1C2" x="112" y="6" font="FFVIIFont" visible="true" />
+            <text_area name="R1C3" x="200" y="6" font="FFVIIFont" visible="true" />
+            <text_area name="R1C4" x="288" y="6" font="FFVIIFont" visible="true" />
+            <text_area name="R2C1" x="24" y="23" font="FFVIIFont" visible="true" />
+            <text_area name="R2C2" x="112" y="23" font="FFVIIFont" visible="true" />
+            <text_area name="R2C3" x="200" y="23" font="FFVIIFont" visible="true" />
+            <text_area name="R2C4" x="288" y="23" font="FFVIIFont" visible="true" />
+            <text_area name="R3C1" x="24" y="40" font="FFVIIFont" visible="true" />
+            <text_area name="R3C2" x="112" y="40" font="FFVIIFont" visible="true" />
+            <text_area name="R3C3" x="200" y="40" font="FFVIIFont" visible="true" />
+            <text_area name="R3C4" x="288" y="40" font="FFVIIFont" visible="true" />
+            <text_area name="R4C1" x="24" y="57" font="FFVIIFont" visible="true" />
+            <text_area name="R4C2" x="112" y="57" font="FFVIIFont" visible="true" />
+            <text_area name="R4C3" x="200" y="57" font="FFVIIFont" visible="true" />
+            <text_area name="R4C4" x="288" y="57" font="FFVIIFont" visible="true" />
+            <text_area name="R5C1" x="24" y="74" font="FFVIIFont" visible="true" />
+            <text_area name="R5C2" x="112" y="74" font="FFVIIFont" visible="true" />
+            <text_area name="R5C3" x="200" y="74" font="FFVIIFont" visible="true" />
+            <text_area name="R5C4" x="288" y="74" font="FFVIIFont" visible="true" />
+            <text_area name="R6C1" x="24" y="91" font="FFVIIFont" visible="true" />
+            <text_area name="R6C2" x="112" y="91" font="FFVIIFont" visible="true" />
+            <text_area name="R6C3" x="200" y="91" font="FFVIIFont" visible="true" />
+            <text_area name="R6C4" x="288" y="91" font="FFVIIFont" visible="true" />
+            <text_area name="R7C1" x="24" y="108" font="FFVIIFont" visible="true" />
+            <text_area name="R7C2" x="112" y="108" font="FFVIIFont" visible="true" />
+            <text_area name="R7C3" x="200" y="108" font="FFVIIFont" visible="true" />
+            <text_area name="R7C4" x="288" y="108" font="FFVIIFont" visible="true" />
+            <text_area name="R8C1" x="24" y="125" font="FFVIIFont" visible="true" />
+            <text_area name="R8C2" x="112" y="125" font="FFVIIFont" visible="true" />
+            <text_area name="R8C3" x="200" y="125" font="FFVIIFont" visible="true" />
+            <text_area name="R8C4" x="288" y="125" font="FFVIIFont" visible="true" />
+            <text_area name="R9C1" x="24" y="142" font="FFVIIFont" visible="true" />
+            <text_area name="R9C2" x="112" y="142" font="FFVIIFont" visible="true" />
+            <text_area name="R9C3" x="200" y="142" font="FFVIIFont" visible="true" />
+            <text_area name="R9C4" x="288" y="142" font="FFVIIFont" visible="true" />
+            <sprite name="Cursor" image="icons/cursor.png" x="-2" y="4" width="24" height="17" visible="false">
+                <animation name="Position1-1" length="0" x="0:-2" y="0:4" />
+                <animation name="Position1-2" length="0" x="0:86" y="0:4" />
+                <animation name="Position1-3" length="0" x="0:174" y="0:4" />
+                <animation name="Position1-4" length="0" x="0:262" y="0:4" />
+                <animation name="Position2-1" length="0" x="0:-2" y="0:21" />
+                <animation name="Position2-2" length="0" x="0:86" y="0:21" />
+                <animation name="Position2-3" length="0" x="0:174" y="0:21" />
+                <animation name="Position2-4" length="0" x="0:262" y="0:21" />
+                <animation name="Position3-1" length="0" x="0:-2" y="0:38" />
+                <animation name="Position3-2" length="0" x="0:86" y="0:38" />
+                <animation name="Position3-3" length="0" x="0:174" y="0:38" />
+                <animation name="Position3-4" length="0" x="0:262" y="0:38" />
+                <animation name="Position4-1" length="0" x="0:-2" y="0:55" />
+                <animation name="Position4-2" length="0" x="0:86" y="0:55" />
+                <animation name="Position4-3" length="0" x="0:174" y="0:55" />
+                <animation name="Position4-4" length="0" x="0:262" y="0:55" />
+                <animation name="Position5-1" length="0" x="0:-2" y="0:72" />
+                <animation name="Position5-2" length="0" x="0:86" y="0:72" />
+                <animation name="Position5-3" length="0" x="0:174" y="0:72" />
+                <animation name="Position5-4" length="0" x="0:262" y="0:72" />
+                <animation name="Position6-1" length="0" x="0:-2" y="0:89" />
+                <animation name="Position6-2" length="0" x="0:86" y="0:89" />
+                <animation name="Position6-3" length="0" x="0:174" y="0:89" />
+                <animation name="Position6-4" length="0" x="0:262" y="0:89" />
+                <animation name="Position7-1" length="0" x="0:-2" y="0:106" />
+                <animation name="Position7-2" length="0" x="0:86" y="0:106" />
+                <animation name="Position7-3" length="0" x="0:174" y="0:106" />
+                <animation name="Position7-4" length="0" x="0:262" y="0:106" />
+                <animation name="Position8-1" length="0" x="0:-2" y="0:123" />
+                <animation name="Position8-2" length="0" x="0:86" y="0:123" />
+                <animation name="Position8-3" length="0" x="0:174" y="0:123" />
+                <animation name="Position8-4" length="0" x="0:262" y="0:123" />
+                <animation name="Position9-1" length="0" x="0:-2" y="0:140" />
+                <animation name="Position9-2" length="0" x="0:86" y="0:140" />
+                <animation name="Position9-3" length="0" x="0:174" y="0:140" />
+                <animation name="Position9-4" length="0" x="0:262" y="0:140" />
+            </sprite>
+        </widget>
+        <widget name="Targets" x="0" y="0" width="50%" height="100%" visible="false">
+            <prototype name="Window" />
+            <widget name="Slot1" x="0" y="14" width="100%" height="64" visible="true">
+                <sprite name="Portrait" image="other/choco.png" x="20" y="0" width="42" height="48" visible="true" />
+                <widget name="Data" x="68" y="2" width="100%-68" height="100%-2" visible="true">
+                    <prototype name="MenuCharacterData" />
+                </widget>
+            </widget>
+            <widget name="Slot2" x="0" y="33%+14" width="100%" height="64" visible="true">
+                <sprite name="Portrait" image="other/choco.png" x="20" y="0" width="42" height="48" visible="true" />
+                <widget name="Data" x="68" y="2" width="100%-68" height="100%-2" visible="true">
+                    <prototype name="MenuCharacterData" />
+                </widget>
+            </widget>
+            <widget name="Slot3" x="0" y="66%+14" width="100%" height="64" visible="true">
+                <sprite name="Portrait" image="other/choco.png" x="20" y="0" width="42" height="48" visible="true" />
+                <widget name="Data" x="68" y="2" width="100%-68" height="100%-2" visible="true">
+                    <prototype name="MenuCharacterData" />
+                </widget>
+            </widget>
+            <sprite name="Cursor" image="icons/cursor.png" x="-2" y="26" width="24" height="17" visible="true">
+                <animation name="Position1" length="0" y="0:26" />
+                <animation name="Position2" length="0" y="0:33%+26" />
+                <animation name="Position3" length="0" y="0:66%+26" />
+            </sprite>
+        </widget>
+    </widget>
+</screen>

+ 2 - 2
data/data/screens/main_menu/main_menu.xml

@@ -23,7 +23,7 @@
                 <sprite name="LimitLinePart2" x="183" y="53" width="25" height="3" colours="0.5 0.125 0.31, 0.5 0.125 0.31, 0 0 0, 0 0 0" alpha="0.8" visible="false" />
             </widget>
             <widget name="Character2" x="2%" y="32%" width="100%" height="32%" visible="true">
-                <sprite name="Portrait" image="other/choco.png" x="5" y="12" width="56" height="56" visible="true">
+                <sprite name="Portrait" image="other/choco.png" x="5" y="12" width="49" height="56" visible="true">
                     <animation name="RowFront" length="0" x="0:5" />
                     <animation name="RowBack" length="0" x="0:25" />
                 </sprite>
@@ -41,7 +41,7 @@
                 <sprite name="LimitLinePart2" x="183" y="53" width="25" height="3" colours="0.5 0.125 0.31, 0.5 0.125 0.31, 0 0 0, 0 0 0" alpha="0.8" visible="false" />
             </widget>
             <widget name="Character3" x="2%" y="62%" width="100%" height="32%" visible="true">
-               <sprite name="Portrait" image="other/choco.png" x="5" y="12" width="56" height="56" visible="true">
+               <sprite name="Portrait" image="other/choco.png" x="5" y="12" width="49" height="56" visible="true">
                     <animation name="RowFront" length="0" x="0:5" />
                     <animation name="RowBack" length="0" x="0:25" />
                 </sprite>

+ 99 - 0
data/data/screens/phs_menu/phs_menu.xml

@@ -0,0 +1,99 @@
+<screen name="PhsMenu" script="scripts/menu/phs_menu.lua" visible="false">
+    <widget name="Container" width="40%" height="40%" x="0" y="0" align="left" valign="top" scale="2.5 2.5" visible="true">
+        <widget name="Help" x="0" y="0" width="100%-81" height="24" visible="true">
+            <prototype name="Window" />
+            <text_area name="Help" text_name="PhsMenuHelp" x="12" y="6" font="FFVIIFont" visible="true" />
+        </widget>
+        <widget name="Title" x="100%-81" y="0" width="81" height="24" visible="true">
+            <prototype name="Window" />
+            <text_area name="Title" text_name="PhsMenuTitle" x="24" y="6" font="FFVIIFont" visible="true" />
+        </widget>
+        <widget name="Party" x="0" y="24" width="180" height="100%-24" visible="true">
+            <prototype name="Window" />
+            <widget name="Slot1" x="0" y="20" width="100%" height="64" visible="true">
+                <sprite name="Portrait" image="other/choco.png" x="16" y="0" width="42" height="48" visible="true" />
+                <widget name="Data" x="64" y="2" width="100%-64" height="100%-2" visible="true">
+                    <prototype name="MenuCharacterData" />
+                </widget>
+            </widget>
+            <widget name="Slot2" x="0" y="33%+20" width="100%" height="64" visible="true">
+                <sprite name="Portrait" image="other/choco.png" x="16" y="0" width="42" height="48" visible="true" />
+                <widget name="Data" x="64" y="2" width="100%-64" height="100%-2" visible="true">
+                    <prototype name="MenuCharacterData" />
+                </widget>
+            </widget>
+            <widget name="Slot3" x="0" y="66%+20" width="100%" height="64" visible="true">
+                <sprite name="Portrait" image="other/choco.png" x="16" y="0" width="42" height="48" visible="true" />
+                <widget name="Data" x="64" y="2" width="100%-64" height="100%-2" visible="true">
+                    <prototype name="MenuCharacterData" />
+                </widget>
+            </widget>
+        </widget>
+        <widget name="Info" x="180" y="24" width="100%-180" height="56" visible="true">
+            <prototype name="Window" />
+            <widget name="Character" x="0" y="0" width="100%" height="100%" visible="false">
+                <widget name="Data" x="30" y="6" width="100%-30" height="100%-6" visible="true">
+                    <prototype name="MenuCharacterData" />
+                </widget>
+            </widget>
+        </widget>
+        <widget name="Reserve" x="180" y="80" width="100%-180" height="100%-80" visible="true">
+            <prototype name="Window" />
+            <widget name="Slot1" x="20" y="10" width="44" height="58" visible="true">
+                <sprite name="Portrait" image="other/choco.png" x="0" y="0" width="42" height="56" visible="true" />
+            </widget>
+            <widget name="Slot2" x="80" y="10" width="44" height="58" visible="true">
+                <sprite name="Portrait" image="other/choco.png" x="0" y="0" width="42" height="56" visible="true" />
+            </widget>
+            <widget name="Slot3" x="140" y="10" width="44" height="58" visible="true">
+                <sprite name="Portrait" image="other/choco.png" x="0" y="0" width="42" height="56" visible="true" />
+            </widget>
+            <widget name="Slot4" x="20" y="76" width="44" height="58" visible="true">
+                <sprite name="Portrait" image="other/choco.png" x="0" y="0" width="42" height="56" visible="true" />
+            </widget>
+            <widget name="Slot5" x="80" y="76" width="44" height="58" visible="true">
+                <sprite name="Portrait" image="other/choco.png" x="0" y="0" width="42" height="56" visible="true" />
+            </widget>
+            <widget name="Slot6" x="140" y="76" width="44" height="58" visible="true">
+                <sprite name="Portrait" image="other/choco.png" x="0" y="0" width="42" height="56" visible="true" />
+            </widget>
+            <widget name="Slot7" x="20" y="142" width="44" height="58" visible="true">
+                <sprite name="Portrait" image="other/choco.png" x="0" y="0" width="42" height="56" visible="true" />
+            </widget>
+            <widget name="Slot8" x="80" y="142" width="44" height="58" visible="true">
+                <sprite name="Portrait" image="other/choco.png" x="0" y="0" width="42" height="56" visible="true" />
+            </widget>
+            <widget name="Slot9" x="140" y="142" width="44" height="58" visible="true">
+                <sprite name="Portrait" image="other/choco.png" x="0" y="0" width="42" height="56" visible="true" />
+            </widget>
+        </widget>
+        <sprite name="Held" image="icons/cursor.png" x="-4" y="56" width="24" height="17" alpha="0.6" visible="false">
+            <animation name="Position1" length="0" x="0:-4" y="0:56" />
+            <animation name="Position2" length="0" x="0:-4" y="0:33%+48" />
+            <animation name="Position3" length="0" x="0:-4" y="0:66%+40" />
+            <animation name="Position4" length="0" x="0:178" y="0:106" />
+            <animation name="Position5" length="0" x="0:238" y="0:106" />
+            <animation name="Position6" length="0" x="0:298" y="0:106" />
+            <animation name="Position7" length="0" x="0:178" y="0:172" />
+            <animation name="Position8" length="0" x="0:238" y="0:172" />
+            <animation name="Position9" length="0" x="0:298" y="0:172" />
+            <animation name="Position10" length="0" x="0:178" y="0:238" />
+            <animation name="Position11" length="0" x="0:238" y="0:238" />
+            <animation name="Position12" length="0" x="0:298" y="0:238" />
+        </sprite>
+        <sprite name="Cursor" image="icons/cursor.png" x="-4" y="56" width="24" height="17" visible="true">
+            <animation name="Position1" length="0" x="0:-4" y="0:56" />
+            <animation name="Position2" length="0" x="0:-4" y="0:33%+48" />
+            <animation name="Position3" length="0" x="0:-4" y="0:66%+40" />
+            <animation name="Position4" length="0" x="0:178" y="0:106" />
+            <animation name="Position5" length="0" x="0:238" y="0:106" />
+            <animation name="Position6" length="0" x="0:298" y="0:106" />
+            <animation name="Position7" length="0" x="0:178" y="0:172" />
+            <animation name="Position8" length="0" x="0:238" y="0:172" />
+            <animation name="Position9" length="0" x="0:298" y="0:172" />
+            <animation name="Position10" length="0" x="0:178" y="0:238" />
+            <animation name="Position11" length="0" x="0:238" y="0:238" />
+            <animation name="Position12" length="0" x="0:298" y="0:238" />
+        </sprite>
+    </widget>
+</screen>

+ 11 - 0
data/data/screens/prototypes.xml

@@ -1,5 +1,16 @@
 <prototypes>
     <prototype name="Window">
+        <sprite name="Top" image="window/b_t.png" x="8" y="0" width="100%-16" height="8" visible="true" />
+        <sprite name="Bottom" image="window/b_b.png" x="8" y="100%-8" width="100%-16" height="8" visible="true" />
+        <sprite name="Left" image="window/b_l.png" x="0" y="8" width="8" height="100%-16" visible="true" />
+        <sprite name="Right" image="window/b_r.png" x="100%-8" y="8" width="8" height="100%-16" visible="true" />
+        <sprite name="TopLeft" image="window/c_tl.png" x="0" y="0" width="8" height="8" visible="true" />
+        <sprite name="TopRight" image="window/c_tr.png" x="100%-8" y="0" width="8" height="8" visible="true" />
+        <sprite name="BottomLeft" image="window/c_bl.png" x="0" y="100%-8" width="8" height="8" visible="true" />
+        <sprite name="BottomRight" image="window/c_br.png" x="100%-8" y="100%-8" width="8" height="8" visible="true" />
+        <sprite name="Center" x="3" y="2" width="100%-6" height="100%-5" colours="0 0 0.7,0 0 0.5,0 0 0.125,0 0 0.31" window_background="true" visible="true" />
+    </prototype>
+    <prototype name="WindowPreview">
         <sprite name="Top" image="window/b_t.png" x="8" y="0" width="100%-16" height="8" visible="true" />
         <sprite name="Bottom" image="window/b_b.png" x="8" y="100%-8" width="100%-16" height="8" visible="true" />
         <sprite name="Left" image="window/b_l.png" x="0" y="8" width="8" height="100%-16" visible="true" />

+ 15 - 0
data/data/screens/quit_menu/quit_menu.xml

@@ -0,0 +1,15 @@
+<screen name="QuitMenu" script="scripts/menu/quit_menu.lua" visible="false">
+    <widget name="Container" width="40%" height="40%" x="0" y="0" align="left" valign="top" scale="2.5 2.5" visible="true">
+        <widget name="Confirm" x="50" y="80" width="220" height="70" visible="true">
+            <prototype name="Window" />
+            <text_area name="Line1" text_name="QuitMenuLine1" x="12" y="8" font="FFVIIFont" visible="true" />
+            <text_area name="Line2" text_name="QuitMenuLine2" x="12" y="22" font="FFVIIFont" visible="true" />
+            <text_area name="Yes" text_name="Yes" x="95" y="38" font="FFVIIFont" visible="true" />
+            <text_area name="No" text_name="No" x="95" y="50" font="FFVIIFont" visible="true" />
+            <sprite name="Cursor" image="icons/cursor.png" x="68" y="36" width="24" height="17" visible="true">
+                <animation name="Position1" length="0" y="0:36" />
+                <animation name="Position2" length="0" y="0:48" />
+            </sprite>
+        </widget>
+    </widget>
+</screen>

+ 241 - 0
data/data/screens/status_menu/status_menu.xml

@@ -0,0 +1,241 @@
+<screen name="StatusMenu" script="scripts/menu/status_menu.lua" visible="false">
+    <widget name="Container" width="40%" height="40%" x="0" y="0" align="left" valign="top" scale="2.5 2.5" visible="true">
+        <widget name="Character" width="100%" height="70" visible="true">
+            <prototype name="Window" />
+            <sprite name="Portrait" image="other/choco.png" x="10" y="12" width="42" height="48" visible="true" />
+            <widget name="Data" x="66" y="14" width="100%-66" height="100%-14" visible="true">
+                <prototype name="MenuCharacterData" />
+            </widget>
+            <text_area name="ExpLbl" text_name="StatusMenuExp" x="170" y="6" font="FFVIIFont" visible="true" />
+            <text_area name="Exp" x="220" y="8" font="FFVIIMenuDigits" visible="true" />
+            <sprite name="NextLevelBar" image="icons/menu_bar.png" x="180" y="19" width="56" height="8" visible="true" />
+            <sprite name="NextLevelLinePart1" x="181" y="20" width="20" height="3" colours="0.5 0.5 0.5, 0.5 0.5 0.5, 0.5 0.125 0.125, 0.5 0.125 0.125" alpha="0.8" visible="false" />
+            <sprite name="NextLevelLinePart2" x="181" y="23" width="20" height="3" colours="0.5 0.125 0.125, 0.5 0.125 0.125, 0 0 0, 0 0 0" alpha="0.8" visible="false" />
+            <text_area name="NextLbl" text_name="StatusMenuNextLevel" x="170" y="32" font="FFVIIFont" visible="true" />
+            <text_area name="Next" x="220" y="34" font="FFVIIMenuDigits" visible="true" />
+            <text_area name="LimitLbl" text_name="StatusMenuLimitLevel" x="170" y="47" font="FFVIIFont" visible="true" />
+            <text_area name="LimitLevel" x="230" y="49" font="FFVIIMenuDigits" visible="true" />
+            <sprite name="LimitLevelBar" image="icons/menu_bar.png" x="259" y="48" width="56" height="8" visible="true" />
+            <sprite name="LimitLinePart1" x="260" y="49" width="25" height="3" colours="0.5 0.5 0.5, 0.5 0.5 0.5, 0.5 0.125 0.31, 0.5 0.125 0.31" alpha="0.8" visible="false" />
+            <sprite name="LimitLinePart2" x="260" y="52" width="25" height="3" colours="0.5 0.125 0.31, 0.5 0.125 0.31, 0 0 0, 0 0 0" alpha="0.8" visible="false" />
+        </widget>
+        <widget name="Title" x="100%-70" y="0" width="70" height="22" visible="true">
+            <prototype name="Window" />
+            <text_area name="Title" text_name="StatusMenuTitle" x="12" y="5" font="FFVIIFont" visible="true" />
+        </widget>
+        <widget name="Page1" x="0" y="70" width="100%" height="100%-70" visible="true">
+            <widget name="Body" x="0" y="0" width="100%" height="100%" visible="true">
+                <prototype name="Window" />
+            </widget>
+            <widget name="Attributes" x="0" y="0" width="150" height="100%" visible="true">
+                <text_area name="StrLbl" text_name="StatusMenuStr" x="28" y="8" font="FFVIIFont" visible="true" />
+                <text_area name="Str" x="100" y="9" font="FFVIIMenuDigits" visible="true" />
+                <text_area name="DexLbl" text_name="StatusMenuDex" x="28" y="20" font="FFVIIFont" visible="true" />
+                <text_area name="Dex" x="100" y="21" font="FFVIIMenuDigits" visible="true" />
+                <text_area name="VitLbl" text_name="StatusMenuVit" x="28" y="32" font="FFVIIFont" visible="true" />
+                <text_area name="Vit" x="100" y="33" font="FFVIIMenuDigits" visible="true" />
+                <text_area name="MagLbl" text_name="StatusMenuMag" x="28" y="44" font="FFVIIFont" visible="true" />
+                <text_area name="Mag" x="100" y="45" font="FFVIIMenuDigits" visible="true" />
+                <text_area name="SprLbl" text_name="StatusMenuSpr" x="28" y="56" font="FFVIIFont" visible="true" />
+                <text_area name="Spr" x="100" y="57" font="FFVIIMenuDigits" visible="true" />
+                <text_area name="LckLbl" text_name="StatusMenuLck" x="28" y="68" font="FFVIIFont" visible="true" />
+                <text_area name="Lck" x="100" y="69" font="FFVIIMenuDigits" visible="true" />
+                <text_area name="AtkLbl" text_name="StatusMenuAttack" x="28" y="85" font="FFVIIFont" visible="true" />
+                <text_area name="Atk" x="100" y="86" font="FFVIIMenuDigits" visible="true" />
+                <text_area name="AtkPctLbl" text_name="StatusMenuAttackPct" x="28" y="97" font="FFVIIFont" visible="true" />
+                <text_area name="AtkPct" x="100" y="98" font="FFVIIMenuDigits" visible="true" />
+                <text_area name="DefLbl" text_name="StatusMenuDefense" x="28" y="109" font="FFVIIFont" visible="true" />
+                <text_area name="Def" x="100" y="110" font="FFVIIMenuDigits" visible="true" />
+                <text_area name="DefPctLbl" text_name="StatusMenuDefensePct" x="28" y="121" font="FFVIIFont" visible="true" />
+                <text_area name="DefPct" x="100" y="122" font="FFVIIMenuDigits" visible="true" />
+                <text_area name="MAtkLbl" text_name="StatusMenuMagicAtk" x="28" y="133" font="FFVIIFont" visible="true" />
+                <text_area name="MAtk" x="100" y="134" font="FFVIIMenuDigits" visible="true" />
+                <text_area name="MDefLbl" text_name="StatusMenuMagicDef" x="28" y="145" font="FFVIIFont" visible="true" />
+                <text_area name="MDef" x="100" y="146" font="FFVIIMenuDigits" visible="true" />
+                <text_area name="MDefPctLbl" text_name="StatusMenuMagicDefPct" x="28" y="157" font="FFVIIFont" visible="true" />
+                <text_area name="MDefPct" x="100" y="158" font="FFVIIMenuDigits" visible="true" />
+            </widget>
+            <widget name="Equipment" x="0" y="0" width="100%" height="100%" visible="true">
+                <widget name="Wpn" x="0" y="85" width="100%" height="32" visible="true">
+                    <sprite name="Icon" image="icons/item_weapon_0.png" x="162" y="-3" width="18" height="18" visible="true" />
+                    <text_area name="Lbl" x="196" y="0" font="FFVIIFont" visible="true" />
+                    <widget name="Slots" x="188" y="17" width="120" height="14" visible="true">
+                        <sprite name="Slot1" image="icons/materia_slot.png" x="0" y="0" width="14" height="14" visible="true"/>
+                        <sprite name="Slot2" image="icons/materia_slot.png" x="15" y="0" width="14" height="14" visible="true"/>
+                        <sprite name="Link1" image="icons/materia_slot_link.png" x="10" y="0" width="8" height="14" visible="true"/>
+                        <sprite name="Slot3" image="icons/materia_slot.png" x="30" y="0" width="14" height="14" visible="true"/>
+                        <sprite name="Slot4" image="icons/materia_slot.png" x="45" y="0" width="14" height="14" visible="true"/>
+                        <sprite name="Link2" image="icons/materia_slot_link.png" x="40" y="0" width="8" height="14" visible="true"/>
+                        <sprite name="Slot5" image="icons/materia_slot.png" x="60" y="0" width="14" height="14" visible="true"/>
+                        <sprite name="Slot6" image="icons/materia_slot.png" x="75" y="0" width="14" height="14" visible="true"/>
+                        <sprite name="Link3" image="icons/materia_slot_link.png" x="70" y="0" width="8" height="14" visible="true"/>
+                        <sprite name="Slot7" image="icons/materia_slot.png" x="90" y="0" width="14" height="14" visible="true"/>
+                        <sprite name="Slot8" image="icons/materia_slot.png" x="105" y="0" width="14" height="14" visible="true"/>
+                        <sprite name="Link4" image="icons/materia_slot_link.png" x="100" y="0" width="8" height="14" visible="true"/>
+                        <sprite name="Mat1" image="icons/materia_3.png" x="2" y="2" width="10" height="10" visible="true"/>
+                        <sprite name="Mat2" image="icons/materia_3.png" x="17" y="2" width="10" height="10" visible="true"/>
+                        <sprite name="Mat3" image="icons/materia_3.png" x="32" y="2" width="10" height="10" visible="true"/>
+                        <sprite name="Mat4" image="icons/materia_3.png" x="47" y="2" width="10" height="10" visible="true"/>
+                        <sprite name="Mat5" image="icons/materia_3.png" x="62" y="2" width="10" height="10" visible="true"/>
+                        <sprite name="Mat6" image="icons/materia_3.png" x="77" y="2" width="10" height="10" visible="true"/>
+                        <sprite name="Mat7" image="icons/materia_3.png" x="92" y="2" width="10" height="10" visible="true"/>
+                        <sprite name="Mat8" image="icons/materia_3.png" x="107" y="2" width="10" height="10" visible="true"/>
+                    </widget>
+                </widget>
+                <widget name="Arm" x="0" y="118" width="100%" height="32" visible="true">
+                    <sprite name="Icon" image="icons/item_armor.png" x="162" y="-3" width="18" height="18" visible="true" />
+                    <text_area name="Lbl" x="196" y="0" font="FFVIIFont" visible="true" />
+                    <widget name="Slots" x="188" y="17" width="120" height="14" visible="true">
+                        <sprite name="Slot1" image="icons/materia_slot.png" x="0" y="0" width="14" height="14" visible="true"/>
+                        <sprite name="Slot2" image="icons/materia_slot.png" x="15" y="0" width="14" height="14" visible="true"/>
+                        <sprite name="Link1" image="icons/materia_slot_link.png" x="10" y="0" width="8" height="14" visible="true"/>
+                        <sprite name="Slot3" image="icons/materia_slot.png" x="30" y="0" width="14" height="14" visible="true"/>
+                        <sprite name="Slot4" image="icons/materia_slot.png" x="45" y="0" width="14" height="14" visible="true"/>
+                        <sprite name="Link2" image="icons/materia_slot_link.png" x="40" y="0" width="8" height="14" visible="true"/>
+                        <sprite name="Slot5" image="icons/materia_slot.png" x="60" y="0" width="14" height="14" visible="true"/>
+                        <sprite name="Slot6" image="icons/materia_slot.png" x="75" y="0" width="14" height="14" visible="true"/>
+                        <sprite name="Link3" image="icons/materia_slot_link.png" x="70" y="0" width="8" height="14" visible="true"/>
+                        <sprite name="Slot7" image="icons/materia_slot.png" x="90" y="0" width="14" height="14" visible="true"/>
+                        <sprite name="Slot8" image="icons/materia_slot.png" x="105" y="0" width="14" height="14" visible="true"/>
+                        <sprite name="Link4" image="icons/materia_slot_link.png" x="100" y="0" width="8" height="14" visible="true"/>
+                        <sprite name="Mat1" image="icons/materia_3.png" x="2" y="2" width="10" height="10" visible="true"/>
+                        <sprite name="Mat2" image="icons/materia_3.png" x="17" y="2" width="10" height="10" visible="true"/>
+                        <sprite name="Mat3" image="icons/materia_3.png" x="32" y="2" width="10" height="10" visible="true"/>
+                        <sprite name="Mat4" image="icons/materia_3.png" x="47" y="2" width="10" height="10" visible="true"/>
+                        <sprite name="Mat5" image="icons/materia_3.png" x="62" y="2" width="10" height="10" visible="true"/>
+                        <sprite name="Mat6" image="icons/materia_3.png" x="77" y="2" width="10" height="10" visible="true"/>
+                        <sprite name="Mat7" image="icons/materia_3.png" x="92" y="2" width="10" height="10" visible="true"/>
+                        <sprite name="Mat8" image="icons/materia_3.png" x="107" y="2" width="10" height="10" visible="true"/>
+                    </widget>
+                </widget>
+                <widget name="Acc" x="0" y="150" width="100%" height="14" visible="true">
+                    <sprite name="Icon" image="icons/item_accessory.png" x="162" y="-3" width="18" height="18" visible="true" />
+                    <text_area name="Lbl" x="196" y="0" font="FFVIIFont" visible="true" />
+                </widget>
+            </widget>
+        </widget>
+        <widget name="Page2" x="0" y="70" width="100%" height="100%-70" visible="false">
+            <widget name="Body" x="0" y="0" width="100%" height="100%" visible="true">
+                <prototype name="Window" />
+            </widget>
+            <widget name="Table" x="0" y="0" width="100%" height="100%" visible="true">
+                <text_area name="Heading" text_name="StatusMenuElement" x="8" y="3" font="FFVIIFont" visible="true" />
+                <widget name="Attack" x="28" y="26" width="100%-28" height="42" visible="true">
+                    <text_area name="Lbl" text_name="StatusMenuAttackGroup" x="0" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name1" x="62" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name2" x="122" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name3" x="182" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name4" x="62" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name5" x="122" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name6" x="182" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name7" x="62" y="28" font="FFVIIFont" visible="true" />
+                    <text_area name="Name8" x="122" y="28" font="FFVIIFont" visible="true" />
+                    <text_area name="Name9" x="182" y="28" font="FFVIIFont" visible="true" />
+                </widget>
+                <widget name="Halve" x="28" y="74" width="100%-28" height="42" visible="true">
+                    <text_area name="Lbl" text_name="StatusMenuHalve" x="0" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name1" x="62" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name2" x="122" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name3" x="182" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name4" x="62" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name5" x="122" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name6" x="182" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name7" x="62" y="28" font="FFVIIFont" visible="true" />
+                    <text_area name="Name8" x="122" y="28" font="FFVIIFont" visible="true" />
+                    <text_area name="Name9" x="182" y="28" font="FFVIIFont" visible="true" />
+                </widget>
+                <widget name="Invalid" x="28" y="122" width="100%-28" height="42" visible="true">
+                    <text_area name="Lbl" text_name="StatusMenuInvalid" x="0" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name1" x="62" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name2" x="122" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name3" x="182" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name4" x="62" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name5" x="122" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name6" x="182" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name7" x="62" y="28" font="FFVIIFont" visible="true" />
+                    <text_area name="Name8" x="122" y="28" font="FFVIIFont" visible="true" />
+                    <text_area name="Name9" x="182" y="28" font="FFVIIFont" visible="true" />
+                </widget>
+                <widget name="Absorb" x="28" y="170" width="100%-28" height="42" visible="true">
+                    <text_area name="Lbl" text_name="StatusMenuAbsorb" x="0" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name1" x="62" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name2" x="122" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name3" x="182" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name4" x="62" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name5" x="122" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name6" x="182" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name7" x="62" y="28" font="FFVIIFont" visible="true" />
+                    <text_area name="Name8" x="122" y="28" font="FFVIIFont" visible="true" />
+                    <text_area name="Name9" x="182" y="28" font="FFVIIFont" visible="true" />
+                </widget>
+            </widget>
+        </widget>
+        <widget name="Page3" x="0" y="70" width="100%" height="100%-70" visible="false">
+            <widget name="Body" x="0" y="0" width="100%" height="100%" visible="true">
+                <prototype name="Window" />
+            </widget>
+            <widget name="Table" x="0" y="0" width="100%" height="100%" visible="true">
+                <widget name="Attack" x="16" y="4" width="100%-16" height="98" visible="true">
+                    <text_area name="Lbl" text_name="StatusMenuAttackGroup" x="0" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Effect" text_name="StatusMenuEffect" x="0" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name1" x="46" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name2" x="132" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name3" x="218" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name4" x="304" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name5" x="46" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name6" x="132" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name7" x="218" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name8" x="304" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name9" x="46" y="28" font="FFVIIFont" visible="true" />
+                    <text_area name="Name10" x="132" y="28" font="FFVIIFont" visible="true" />
+                    <text_area name="Name11" x="218" y="28" font="FFVIIFont" visible="true" />
+                    <text_area name="Name12" x="304" y="28" font="FFVIIFont" visible="true" />
+                    <text_area name="Name13" x="46" y="42" font="FFVIIFont" visible="true" />
+                    <text_area name="Name14" x="132" y="42" font="FFVIIFont" visible="true" />
+                    <text_area name="Name15" x="218" y="42" font="FFVIIFont" visible="true" />
+                    <text_area name="Name16" x="304" y="42" font="FFVIIFont" visible="true" />
+                    <text_area name="Name17" x="46" y="56" font="FFVIIFont" visible="true" />
+                    <text_area name="Name18" x="132" y="56" font="FFVIIFont" visible="true" />
+                    <text_area name="Name19" x="218" y="56" font="FFVIIFont" visible="true" />
+                    <text_area name="Name20" x="304" y="56" font="FFVIIFont" visible="true" />
+                    <text_area name="Name21" x="46" y="70" font="FFVIIFont" visible="true" />
+                    <text_area name="Name22" x="132" y="70" font="FFVIIFont" visible="true" />
+                    <text_area name="Name23" x="218" y="70" font="FFVIIFont" visible="true" />
+                    <text_area name="Name24" x="304" y="70" font="FFVIIFont" visible="true" />
+                    <text_area name="Name25" x="46" y="84" font="FFVIIFont" visible="true" />
+                    <text_area name="Name26" x="132" y="84" font="FFVIIFont" visible="true" />
+                </widget>
+                <widget name="Defend" x="16" y="110" width="100%-16" height="98" visible="true">
+                    <text_area name="Lbl" text_name="StatusMenuDefend" x="0" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Effect" text_name="StatusMenuEffect" x="0" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name1" x="46" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name2" x="132" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name3" x="218" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name4" x="304" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Name5" x="46" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name6" x="132" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name7" x="218" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name8" x="304" y="14" font="FFVIIFont" visible="true" />
+                    <text_area name="Name9" x="46" y="28" font="FFVIIFont" visible="true" />
+                    <text_area name="Name10" x="132" y="28" font="FFVIIFont" visible="true" />
+                    <text_area name="Name11" x="218" y="28" font="FFVIIFont" visible="true" />
+                    <text_area name="Name12" x="304" y="28" font="FFVIIFont" visible="true" />
+                    <text_area name="Name13" x="46" y="42" font="FFVIIFont" visible="true" />
+                    <text_area name="Name14" x="132" y="42" font="FFVIIFont" visible="true" />
+                    <text_area name="Name15" x="218" y="42" font="FFVIIFont" visible="true" />
+                    <text_area name="Name16" x="304" y="42" font="FFVIIFont" visible="true" />
+                    <text_area name="Name17" x="46" y="56" font="FFVIIFont" visible="true" />
+                    <text_area name="Name18" x="132" y="56" font="FFVIIFont" visible="true" />
+                    <text_area name="Name19" x="218" y="56" font="FFVIIFont" visible="true" />
+                    <text_area name="Name20" x="304" y="56" font="FFVIIFont" visible="true" />
+                    <text_area name="Name21" x="46" y="70" font="FFVIIFont" visible="true" />
+                    <text_area name="Name22" x="132" y="70" font="FFVIIFont" visible="true" />
+                    <text_area name="Name23" x="218" y="70" font="FFVIIFont" visible="true" />
+                    <text_area name="Name24" x="304" y="70" font="FFVIIFont" visible="true" />
+                    <text_area name="Name25" x="46" y="84" font="FFVIIFont" visible="true" />
+                    <text_area name="Name26" x="132" y="84" font="FFVIIFont" visible="true" />
+                </widget>
+            </widget>
+        </widget>
+    </widget>
+</screen>

+ 3 - 0
data/data/scripts/_scripts.xml

@@ -15,6 +15,8 @@
     <script file="gamedata/weapons.lua" />
 
     <script file="scripts/system.lua" />
+    <script file="scripts/character_effects.lua" />
+    <script file="scripts/character_abilities.lua" />
     <script file="scripts/save.lua" />
 
     <!-- TODO: Don't always parse initial savemap when loading is implemented. -->
@@ -26,6 +28,7 @@
     <script file="scripts/battle_ai.lua" />
     <script file="scripts/battle/battle_ai_vm.lua" />
     <script file="scripts/menu/menu_helpers.lua" />
+    <script file="scripts/menu/menu_lib.lua" />
 
     <!-- TODO: Debugging pourposes only -->
     <script file="scripts/debug_data.lua" />

+ 25 - 0
data/data/scripts/battle.lua

@@ -103,6 +103,29 @@ Battle.Damage = {
 
 
 
+--- Is the ATB time stopped by the menus the player has open?
+--
+-- Depends on the ATB setting of the config menu (MenuSettings.config.atb):
+--  * 1, active: time never stops.
+--  * 2, recommended: time stops while a list (magic, summon, item) is open.
+--  * 3, wait: time stops while a list is open and while a target is being chosen.
+Battle.is_time_stopped = function()
+    local mode = (MenuSettings.config ~= nil) and MenuSettings.config.atb or 2
+    local ui = UiContainer.BattleUi
+    if mode == 1 or ui == nil then return false end
+    local state = ui.window_state
+    if mode == 2 then return state == "list" end
+    return state == "list" or state == "target"
+end
+
+--- Can the battle camera move by itself? (Camera angle: Auto in the config menu.)
+--
+-- TODO: The battle camera doesn't move during the battle yet. When it does, the movements of the
+-- camera (attacks, magic, summons) must only happen if this returns true.
+Battle.is_camera_auto = function()
+    return MenuSettings.config == nil or MenuSettings.config.camera ~= 2
+end
+
 Battle.logic_script = {
     unit_type = Battle.Type.LOGIC,
 
@@ -133,6 +156,8 @@ Battle.logic_script = {
     on_update = function( self )
         local timer = timer:get_game_time_total()
         local delta = timer - self.game_timer
+        -- The timers don't advance while the menus stop the ATB time.
+        if Battle.is_time_stopped() then delta = 0 end
 
         -- update timers
         for key, value in pairs( EntityContainer ) do

+ 44 - 11
data/data/scripts/battle/battle_ui.lua

@@ -262,6 +262,14 @@ UiContainer.BattleUi = {
     --- Current position for the cursor in the command window.
     cursor_command = 1,
 
+    --- Last command chosen by each party member (position in the command window), by party slot.
+    -- Used when the cursor memory is on.
+    memory_command = {},
+
+    --- Last position chosen by each party member in the lists (magic, summon, item), by party slot
+    -- and list kind: {first = first visible entry, cursor = row of the cursor}.
+    memory_list = {},
+
     --- Current position for the cursor in the list window.
     cursor_list = 1,
 
@@ -302,7 +310,8 @@ UiContainer.BattleUi = {
     --- Handles button events.
     --
     -- For the current submenu, handles directional keys, enter and escape events.
-    -- @param button Pressed button string key. "Up", "Left", "Enter" and "Escape" are handled.
+    -- @param button Game event (the directions, OK, cancel...) or, for the debug keys, the name of
+    -- the key.
     -- @param event Trigger event. Normally, "Press".
     on_button = function(self, button, event)
         if self.active == false then do return 0 end end
@@ -336,7 +345,7 @@ UiContainer.BattleUi = {
         if self.window_state == "command" then
             local command_count = #self.party[self.current_character_window].command
             local cursor = ui_manager:get_widget("BattleUi.Container.Bottom.Commands.Cursor")
-            if button == "Down" then
+            if button == Config.CONTROLS.DOWN then
                 if self.cursor_command < 4 then
                     -- If on the first three ones, always alow to go down to reach "Item"
                     self.cursor_command = self.cursor_command + 1
@@ -358,7 +367,7 @@ UiContainer.BattleUi = {
                     cursor:set_default_animation("Position" .. tostring(self.cursor_command))
                     audio_manager:play_sound("Cursor")
                 end
-            elseif button == "Up" then
+            elseif button == Config.CONTROLS.UP then
                 if self.cursor_command == 1 then
                     -- If on "Attack, always go to item
                     self.cursor_command = 4
@@ -382,7 +391,7 @@ UiContainer.BattleUi = {
                     cursor:set_default_animation("Position" .. tostring(self.cursor_command))
                     audio_manager:play_sound("Cursor")
                 end
-            elseif button == "Left" then
+            elseif button == Config.CONTROLS.LEFT then
                 if command_count > 4 then
                     if self.cursor_command > 4 then
                         self.cursor_command = self.cursor_command - 4
@@ -396,7 +405,7 @@ UiContainer.BattleUi = {
                         audio_manager:play_sound("Cursor")
                     end
                 end
-            elseif button == "Right" then
+            elseif button == Config.CONTROLS.RIGHT then
                 if command_count > 4 then
                     self.cursor_command = self.cursor_command + 4
                     if self.cursor_command > command_count then
@@ -417,6 +426,7 @@ UiContainer.BattleUi = {
                 audio_manager:play_sound("Cursor")
                 self:show_commands(self.command_queue[1])
             elseif button == Config.ACTIONS.CONFIRM then
+                self.memory_command[self.current_character_window] = self.cursor_command
                 -- Check if a menu must be shown
                 local cmd = Game.Commands[self.party[self.current_character_window].command[self.cursor_command]]
                 if cmd.menu == Battle.Menu.NONE then print("DIRECT COMMAND")
@@ -440,7 +450,7 @@ UiContainer.BattleUi = {
             if button == Config.ACTIONS.CANCEL then
                 audio_manager:play_sound("Cursor")
                 self:close_list()
-            elseif button == "Down" then
+            elseif button == Config.CONTROLS.DOWN then
                 if total <=  self.cursor_list + self.first_list - 1 then do return 0 end end
                 if self.cursor_list < 4 then
                     -- Move cursor down
@@ -453,7 +463,7 @@ UiContainer.BattleUi = {
                     self.first_list = self.first_list + 1
                     self:repopulate_list()
                 end
-            elseif button == "Up" then
+            elseif button == Config.CONTROLS.UP then
                 if self.cursor_list == 1 and self.first_list == 1 then do return 0 end end
                 if self.cursor_list > 1 then
                     -- Move cursor up
@@ -474,7 +484,7 @@ UiContainer.BattleUi = {
             if button == Config.ACTIONS.CANCEL then
                 audio_manager:play_sound("Cursor")
                 self:close_target()
-            elseif button == "Up" then
+            elseif button == Config.CONTROLS.UP then
                 if self.cursor_target_h == 2 then
                     -- Allies cursor, never scroll. Go un up or to the bottom
                     self.cursor_target_v = self.cursor_target_v - 1
@@ -517,7 +527,7 @@ UiContainer.BattleUi = {
                         end
                     end
                 end
-            elseif button == "Down" then
+            elseif button == Config.CONTROLS.DOWN then
                 if self.cursor_target_h == 2 then
                     -- Allies cursor, never scroll. Go un up or to the bottom
                     self.cursor_target_v = self.cursor_target_v + 1
@@ -560,7 +570,7 @@ UiContainer.BattleUi = {
                         end
                     end
                 end
-            elseif button == "Left" then
+            elseif button == Config.CONTROLS.LEFT then
                 if self.target_options.fixed_row == 0 and self.target_options.toggle_multiple == 0 then
                     -- If both rows are present and multiple toggling is not available...
                     if self.cursor_target_h == 2 then
@@ -600,7 +610,7 @@ UiContainer.BattleUi = {
                     elseif prev_mode == Battle.TargetType.SINGLE then self.target_type = Battle.TargetType.GROUP
                     end
                 end
-            elseif button == "Right" then
+            elseif button == Config.CONTROLS.RIGHT then
                 if self.target_options.fixed_row == 0 and self.target_options.toggle_multiple == 0 then
                     -- If both rows are present and multiple toggling is not available...
                     if self.cursor_target_h == 1 then
@@ -2671,6 +2681,14 @@ UiContainer.BattleUi = {
             self.first_list = 1
             self.current_list = kind
             self.list_entries = self:build_list_entries(kind)
+            -- Start where the list was left, if the cursor memory is on.
+            if UiContainer.cursor_memory() then
+                local remembered = (self.memory_list[self.current_character_window] or {})[kind]
+                if remembered ~= nil and remembered.first + remembered.cursor - 1 <= #self.list_entries then
+                    self.first_list = remembered.first
+                    self.cursor_list = remembered.cursor
+                end
+            end
             self:populate_list()
             self.window_state = "list"
             ui_manager:get_widget("BattleUi.Container.Bottom.List"):set_visible(true)
@@ -2693,8 +2711,17 @@ UiContainer.BattleUi = {
         end
     end,
 
+    --- Remembers the position of the cursor in the current list, for the cursor memory.
+    remember_list = function(self)
+        local slot = self.current_character_window
+        if self.current_list == "none" or slot == nil then return end
+        self.memory_list[slot] = self.memory_list[slot] or {}
+        self.memory_list[slot][self.current_list] = { first = self.first_list, cursor = self.cursor_list }
+    end,
+
     --- Closes the currently opened list
     close_list = function(self)
+        self:remember_list()
         self.current_list = "none"
         self.window_state = "command"
         ui_manager:get_widget("BattleUi.Container.Bottom.List"):set_visible(false)
@@ -2708,6 +2735,7 @@ UiContainer.BattleUi = {
         local entry = nil
         if self.window_state == "list" then entry = self.list_entries[self.cursor_list + self.first_list - 1] end
         if entry == nil then do return end end
+        self:remember_list()
         -- Spells, summons and items that can't be used (not enough MP, no summons left...).
         if not entry.enabled then
             audio_manager:play_sound("Error")
@@ -3168,6 +3196,11 @@ UiContainer.BattleUi = {
         ui_manager:get_widget("BattleUi.Container.Bottom.Commands"):set_visible(false)
         ui_manager:get_widget("BattleUi.Container.Bottom.Commands"):set_width(60 * (1 + math.floor(total_commands / 4)))
         ui_manager:get_widget("BattleUi.Container.Bottom.Commands"):set_visible(true)
+        -- Start at the command chosen last time, if the cursor memory is on.
+        if UiContainer.cursor_memory() then
+            local remembered = self.memory_command[party_id]
+            if remembered ~= nil and remembered <= total_commands then self.cursor_command = remembered end
+        end
         -- Set and show cursor
         ui_manager:get_widget("BattleUi.Container.Bottom.Commands.Cursor"):set_default_animation("Position" .. tostring(self.cursor_command))
         ui_manager:get_widget("BattleUi.Container.Bottom.Commands.Cursor"):set_visible(true)

+ 159 - 0
data/data/scripts/character_abilities.lua

@@ -0,0 +1,159 @@
+--- Abilities of the characters that come from their equipped materia.
+
+--- Command of the Enemy Skill materia (see Game.Commands).
+Characters.E_SKILL_COMMAND = 13
+
+--- ID of the attack of the first enemy skill. There are 24, in the order of the Enemy Skill materia.
+Characters.FIRST_E_SKILL_ATTACK = 72
+
+--- Gets the level of a materia given its AP.
+--
+-- @param materia Definition of the materia (an entry of Game.Materia).
+-- @param ap AP of the materia.
+-- @return The level (1 or more), or 0 if the AP are less than needed for the first level.
+local function get_materia_level(materia, ap)
+    local level = 0
+    for l = 1, #materia.levels_ap do
+        if materia.levels_ap[l] <= ap then level = l end
+    end
+    return level
+end
+
+--- Gets the magic, summons and enemy skills a character can use with the materia that is equipped.
+--
+-- Every ability is listed once, even if more than one materia gives it.
+--
+-- @param char_id ID of the character.
+-- @return A table with these lists, sorted by attack ID:
+--  * magic: Attack IDs of the spells.
+--  * magic_groups: The same spells, in lists with the ones that come from the same materia. The
+--    groups are sorted by their first spell.
+--  * summon: Tables {id = attack ID, times = times it can be used per battle (255 is no limit)}.
+--  * e_skill: Attack IDs of the enemy skills that were learned.
+Characters.get_abilities = function(char_id)
+    local result = { magic = {}, magic_groups = {}, summon = {}, e_skill = {} }
+    local character = Characters[char_id]
+    if character == nil then return result end
+
+    local magic_seen = {}
+    local e_skill_seen = {}
+    local summon_by_id = {}
+
+    for _, part in ipairs({ "weapon", "armor" }) do
+        local equipment = character[part]
+        -- Slots are 0-7.
+        for slot = 0, 7 do
+            local equipped = equipment.materia[slot]
+            local materia = (equipped ~= nil) and Game.Materia[equipped.id] or nil
+            if materia ~= nil then
+                local level = get_materia_level(materia, equipped.ap or 0)
+                if materia.type == Materia.TYPE.MAGIC and materia.magic ~= nil then
+                    local group = {}
+                    for spell_level, spell in ipairs(materia.magic) do
+                        if level >= spell_level and not magic_seen[spell] then
+                            magic_seen[spell] = true
+                            result.magic[#result.magic + 1] = spell
+                            group[#group + 1] = spell
+                        end
+                    end
+                    if #group > 0 then result.magic_groups[#result.magic_groups + 1] = group end
+                elseif materia.type == Materia.TYPE.SUMMON and materia.summon ~= nil and level > 0 then
+                    local times = materia.times[level] or 1
+                    local existing = summon_by_id[materia.summon]
+                    if existing == nil then
+                        existing = { id = materia.summon, times = times }
+                        summon_by_id[materia.summon] = existing
+                        result.summon[#result.summon + 1] = existing
+                    elseif times > existing.times then
+                        existing.times = times
+                    end
+                elseif materia.type == Materia.TYPE.COMMAND and materia.command ~= nil then
+                    for _, command in ipairs(materia.command) do
+                        if command == Characters.E_SKILL_COMMAND and equipped.skills ~= nil then
+                            for skill = 1, 24 do
+                                local attack = Characters.FIRST_E_SKILL_ATTACK + skill - 1
+                                if equipped.skills[skill] == true and not e_skill_seen[attack] then
+                                    e_skill_seen[attack] = true
+                                    result.e_skill[#result.e_skill + 1] = attack
+                                end
+                            end
+                        end
+                    end
+                end
+            end
+        end
+    end
+
+    table.sort(result.magic)
+    table.sort(result.magic_groups, function(a, b) return a[1] < b[1] end)
+    table.sort(result.e_skill)
+    table.sort(result.summon, function(a, b) return a.id < b.id end)
+    return result
+end
+
+--- Status of death (see BattleStatus). The characters with no HP are dead.
+local STATUS_DEATH = 0
+
+--- Checks if an attack revives (cures the death status).
+local function revives(attack)
+    for _, effect in ipairs(attack.status or {}) do
+        -- Mode 1 is cure.
+        if effect.status == STATUS_DEATH and effect.mode == 1 then return true end
+    end
+    return false
+end
+
+--- Gets the maximum of a stat (HP or MP) of a character.
+-- @param stat The stat, with the fields max (if it was calculated) and base.
+local function get_stat_max(stat)
+    return stat.max or stat.base
+end
+
+--- Casts a spell from the menu, out of battle: the cure and life families.
+--
+-- The spell heals (or revives) the target and the caster pays the MP. If the spell can't be cast
+-- (not enough MP, the target is dead for a cure, alive for a life, or already has all its HP), it
+-- does nothing and costs nothing.
+--
+-- The amounts are the ones of the battle: cures heal the magic base times the power / 16, and life
+-- spells a fraction of the maximum HP (power / 32).
+--
+-- @param caster_id ID of the character that casts.
+-- @param attack_id ID of the attack (spell).
+-- @param target_id ID of the character that receives it.
+-- @return True if it was cast, and the HP that were restored.
+Characters.cast_menu_spell = function(caster_id, attack_id, target_id)
+    local attack = Game.Attacks[attack_id]
+    local caster = Characters[caster_id]
+    local target = Characters[target_id]
+    if attack == nil or caster == nil or target == nil then return false end
+
+    local cost = attack.mp or 0
+    if caster.stats.mp.current < cost then return false end
+
+    local hp = target.stats.hp
+    local hp_max = get_stat_max(hp)
+    local dead = hp.current <= 0
+    local power = attack.power or 0
+    local amount = 0
+
+    if revives(attack) then
+        -- Life: only works on dead characters. A quarter of the HP if the power is unknown.
+        if not dead then return false end
+        amount = hp_max * ((power > 0) and power or 8) / 32
+    elseif attack.dmg_formula == 2 and attack.dmg_modifier == 5 then
+        -- Cure: doesn't work on dead characters, or on the ones with all their HP.
+        if dead or hp.current >= hp_max then return false end
+        local mag = caster.stats.mag.total or caster.stats.mag.base or 1
+        local level = caster.level or 1
+        local base = 6 * (mag + ((mag + level) / 32) * ((mag * level) / 32))
+        amount = base * power / 16 * (15 + math.random()) / 16
+    else
+        return false
+    end
+
+    amount = math.max(1, math.min(math.floor(amount), hp_max - hp.current))
+    hp.current = hp.current + amount
+    caster.stats.mp.current = caster.stats.mp.current - cost
+    return true, amount
+end

+ 165 - 0
data/data/scripts/character_effects.lua

@@ -0,0 +1,165 @@
+--- Elemental and status effects a character gets from the equipment and the equipped materia.
+--
+-- Used by the status menu, and usable by anything that needs to know what a character resists or
+-- inflicts.
+
+--- Element IDs, as stored in the item and materia data.
+Characters.ELEMENT = {
+    FIRE = 0, ICE = 1, BOLT = 2, EARTH = 3, POISON = 4, GRAVITY = 5, WATER = 6, WIND = 7,
+    HOLY = 8, RESTORATIVE = 9, CUT = 10, HIT = 11, PUNCH = 12, SHOOT = 13, SHOUT = 14, HIDDEN = 15,
+}
+
+--- How an element affects a character. Lower is stronger.
+Characters.ELEMENT_MODE = {
+    ABSORB = 0,
+    NULLIFY = 1,
+    HALF = 2,
+}
+
+--- Status IDs, as stored in the item and materia data (bit position in the kernel).
+Characters.STATUS = {
+    DEATH = 0, NEAR_DEATH = 1, SLEEP = 2, POISON = 3, SADNESS = 4, FURY = 5, CONFUSION = 6,
+    SILENCE = 7, HASTE = 8, SLOW = 9, STOP = 10, FROG = 11, SMALL = 12, SLOW_NUMB = 13,
+    PETRIFY = 14, REGEN = 15, BARRIER = 16, MBARRIER = 17, REFLECT = 18, DUAL = 19, SHIELD = 20,
+    D_SENTENCE = 21, MANIPULATE = 22, BERSERK = 23, PEERLESS = 24, PARALYZED = 25, DARKNESS = 26,
+}
+
+--- Element used in the materia data for "no element".
+local NO_ELEMENT = Characters.ELEMENT.HIDDEN
+
+--- Materia level (1-based) for an amount of AP.
+local function materia_level(definition, ap)
+    local level = 0
+    for _, level_ap in ipairs(definition.levels_ap) do
+        if level_ap > ap then break end
+        level = level + 1
+    end
+    return math.max(level, 1)
+end
+
+--- Mode an Elemental materia gives to armor, by materia level.
+local function elemental_armor_mode(level)
+    if level >= 3 then return Characters.ELEMENT_MODE.ABSORB end
+    if level == 2 then return Characters.ELEMENT_MODE.NULLIFY end
+    return Characters.ELEMENT_MODE.HALF
+end
+
+--- Adds an element resistance, keeping the strongest one.
+local function add_resistance(defense, element, mode)
+    if mode == nil or mode > Characters.ELEMENT_MODE.HALF then return end
+    if defense[element] == nil or mode < defense[element] then
+        defense[element] = mode
+    end
+end
+
+--- Gets the materia linked to a slot.
+--
+-- @param item The weapon or armor definition.
+-- @param slot Slot of the item, 1-8.
+-- @return List of slots (1-8) linked to it.
+local function linked_slots(item, slot)
+    local linked = {}
+    -- slots[n] == 1 means that slot n is linked to slot n + 1.
+    if item.slots[slot] == 1 and #(item.slots) > slot then table.insert(linked, slot + 1) end
+    if slot > 1 and item.slots[slot - 1] == 1 then table.insert(linked, slot - 1) end
+    return linked
+end
+
+--- Applies the link materia (Elemental, Added Effect) of a weapon or armor.
+local function apply_link_materia(char_id, part, result)
+    local equipment = Characters[char_id][part]
+    local item = Game.Items[equipment.id]
+    if item == nil or item.slots == nil then return end
+    for slot = 1, #(item.slots) do
+        local equipped = equipment.materia[slot - 1]
+        local definition = equipped ~= nil and Game.Materia[equipped.id] or nil
+        if definition ~= nil and definition.link ~= nil then
+            local link = definition.link.id
+            for _, partner_slot in ipairs(linked_slots(item, slot)) do
+                local partner = equipment.materia[partner_slot - 1]
+                local partner_def = partner ~= nil and Game.Materia[partner.id] or nil
+                if partner_def ~= nil then
+                    if link == Materia.LINK.ELEMENTAL then
+                        for _, element in ipairs(partner_def.elements or {}) do
+                            if element ~= NO_ELEMENT then
+                                if part == "weapon" then
+                                    result.elements.attack[element] = true
+                                else
+                                    add_resistance(
+                                        result.elements.defense, element,
+                                        elemental_armor_mode(materia_level(definition, equipped.ap))
+                                    )
+                                end
+                            end
+                        end
+                    elseif link == Materia.LINK.ADDED_EFFECT then
+                        local target = (part == "weapon") and result.status.attack or result.status.defense
+                        for _, status in ipairs(partner_def.status or {}) do
+                            target[status] = true
+                        end
+                    end
+                end
+            end
+        end
+    end
+end
+
+--- Calculates the elemental and status effects of a character.
+--
+-- Takes into account the weapon, the armor, the accessory and the linked materia:
+--  * Weapon elements, and Elemental materia linked in the weapon: elements the attacks have.
+--  * Armor and accessory elements: resistances. Elemental materia linked in the armor gives half
+--    damage (level 1), nullifies (level 2) or absorbs (level 3 or higher).
+--  * Added Effect materia: weapon statuses are inflicted, armor statuses are protected against.
+--  * Accessory statuses: protected against.
+--
+-- @param char_id ID of the character.
+-- @return A table:
+--     elements = {attack = {[element] = true}, defense = {[element] = mode}}
+--     status = {attack = {[status] = true}, defense = {[status] = true}}
+Characters.get_effects = function(char_id)
+    local result = {
+        elements = {attack = {}, defense = {}},
+        status = {attack = {}, defense = {}},
+    }
+    local character = Characters[char_id]
+    if character == nil then return result end
+
+    -- Weapon
+    local weapon = Game.Items[character.weapon.id]
+    if weapon ~= nil then
+        for _, element in ipairs(weapon.elements or {}) do
+            result.elements.attack[element] = true
+        end
+    end
+
+    -- Armor
+    local armor = Game.Items[character.armor.id]
+    if armor ~= nil then
+        for _, element in ipairs(armor.elements or {}) do
+            add_resistance(result.elements.defense, element.id, element.mode)
+        end
+    end
+
+    -- Accessory
+    local accessory = character.accessory ~= nil and Game.Items[character.accessory] or nil
+    if accessory ~= nil then
+        for _, element in ipairs(accessory.elements or {}) do
+            if type(element) == "table" then
+                add_resistance(result.elements.defense, element.id, element.mode)
+            else
+                -- Older data without a mode.
+                add_resistance(result.elements.defense, element, Characters.ELEMENT_MODE.NULLIFY)
+            end
+        end
+        for _, status in ipairs(accessory.status or {}) do
+            result.status.defense[status] = true
+        end
+    end
+
+    -- Materia
+    apply_link_materia(char_id, "weapon", result)
+    apply_link_materia(char_id, "armor", result)
+
+    return result
+end

+ 2 - 33
data/data/scripts/config.lua

@@ -16,36 +16,5 @@ Config = {
         R2 = "R2",
         L1 = "L1",
         L2 = "L2",
-    },
-    WINDOW = {
-        TOP_LEFT = {
-            r = 0,
-            g = 0,
-            b = 178,
-        },
-        TOP_RIGHT = {
-            r = 0,
-            g = 0,
-            b = 127,
-        },
-        BOTTOM_RIGHT = {
-            r = 0,
-            g = 0,
-            b = 32,
-        },
-        BOTTOM_LEFT = {
-            r = 0,
-            g = 0,
-            b = 79,
-        },
-    },
-    CURSOR = "MEMORY",
-    ATB = "ACTIVE",
-    SPEED = {
-        BATTLE = 127,
-        BATTLE_MESSAGE = 127,
-        FIELD_MESSAGE = 127,
-    },
-    CAMERA_ANGLE = "AUTO",
-    MAGIC_ORDER = 1
-} 
+    }
+}

+ 2 - 1
data/data/scripts/debug_data.lua

@@ -44,8 +44,9 @@ Inventory.add_key_item(38)
 end]]
 Inventory.money = 1234567
 Inventory.sort(Inventory.ORDER.NAME)
-Characters[0].stats.hp.current = 12345
+--[[Characters[0].stats.hp.current = 12345
 Characters[0].stats.hp.base = 54321
+]]
 Characters[0].stats.dex.base = 11
 Characters[0].stats.dex.total = 11
 Characters[0].armor.id = 268

+ 5 - 0
data/data/scripts/field.lua

@@ -242,6 +242,11 @@ name_character = function(id)
         print("Tried to rename invalid character with ID " .. tostring(id))
         return 0
     end
+    -- While the name menu is not enabled, the character just gets its default name.
+    if not UiContainer.NameMenu.enabled then
+        UiContainer.NameMenu:apply_default_name(id)
+        return 0
+    end
     UiContainer.NameMenu.id = id
     UiContainer.NameMenu.chocobo = false
     script:request(Script.UI, "NameMenu", "show", 0)

+ 136 - 125
data/data/scripts/menu/begin_menu.lua

@@ -1,141 +1,152 @@
 if UiContainer == nil then UiContainer = {} end
 
-    --- The begin menu
-    UiContainer.BeginMenu = {
+--- The begin menu
+UiContainer.BeginMenu = UiContainer.Menu.new{
 
-        --- Current cursor position
-        position = 1,
+    id = "begin",
 
-        -- Max cursor position
-        position_total = 7,
+    --- Cursor over the entries.
+    cursor = UiContainer.Cursor.new{
+        widget = "BeginMenu.Container.Cursor",
+        total = 7,
+    },
 
-        --- Run when the menu is created.
-        --
-        -- Sets this menu as the one beind displayed.
-        on_start = function(self)
-        UiContainer.current_menu = "begin"
-        local cursor = ui_manager:get_widget("BeginMenu.Container.Cursor")
-        cursor:set_default_animation("Position" .. self.position)
+    --- Run when the menu is created.
+    --
+    -- Sets this menu as the one being displayed.
+    on_start = function(self)
+        self:activate()
+        self.cursor:refresh()
         return 0
+    end,
+
+    --- What each entry does. Indexed by cursor position.
+    actions = {
+        -- 1: New game
+        function(self)
+            audio_manager:play_sound("Window")
+            generate_control_key()
+            load_field_map_request("md1stin", "")
+            console("camera_free false")
+            --console("debug_walkmesh true")
+            script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
+            MenuSettings.pause_available = true
+        end,
+
+        -- 2: Load, show save menu.
+        function(self)
+            audio_manager:play_sound("Cursor")
+            UiContainer.SaveMenu.operation = "load"
+            script:request_end_sync(Script.UI, "SaveMenu", "show", 0)
+        end,
+
+        -- 3: NMKIN_3 test.
+        function(self)
+            Data.progress_game = 27
+            set_party(0, 1, -1)
+            load_field_map_request("nmkin_3", "")
+            console("camera_free false")
+            console("debug_walkmesh true")
+            script:wait(1)
+            script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
+            entity_manager:get_entity("Cloud"):set_position(-1.090750, 15.310307, 17.281244)
+            entity_manager:set_player_entity("Cloud")
+            script:wait(1)
+            entity_manager:get_player_entity():set_position(-1.093750, 15.320307, 17.281244)
+            entity_manager:get_entity("Cloud"):jump_to_position(0.46875, 14.9922, -1, 1, 42) -- Triangle 42, 2 steps.
+            entity_manager:get_entity("Cloud"):jump_sync()
+            entity_manager:player_lock(false)
+            MenuSettings.pause_available = true
+            -- NMKIN_4 test:
+            --[[Data.progress_game = 14
+            --load_field_map_request("nmkin_4", "")
+            map("nmkin_4")
+            console("camera_free false")
+            console("debug_walkmesh true")
+            script:wait(1.5)
+            script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
+            entity_manager:get_entity("Cloud"):set_position(-0.820312, 0.500000, 17.281244)
+            entity_manager:set_player_entity("Cloud")
+            set_party(0, 1, 4)
+            script:wait(0.5)
+            entity_manager:get_player_entity():set_position(-0.820312, 0.500000, 17.281244)
+            entity_manager:get_entity("Cloud"):jump_to_position(-0.800312, 0.500000, -1, 1) -- Triangle 42, 2 steps.
+            entity_manager:get_entity("Cloud"):jump_sync()
+            entity_manager:player_lock(false)
+            print("TRIANGLE " .. entity_manager:get_entity("Cloud"):get_move_triangle_id())
+            MenuSettings.pause_available = true]]
+        end,
+
+        -- 4: Walkmesh test map.
+        function(self)
+            script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
+            console("camera_free true")
+            console("debug_walkmesh true")
+            map("test_2")
+            MenuSettings.pause_available = true
+        end,
+
+        -- 5: World map.
+        function(self)
+            set_party(0, 2, 3)
+            script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
+            console("camera_free true")
+            console("debug_entity 2")
+            MenuSettings.pause_available = true
+            world_map_module:init()
+        end,
+
+        -- 6: Start map.
+        function(self)
+            load_field_map_request("startmap", "")
+            console("camera_free false")
+            console("debug_walkmesh false")
+            script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
+            MenuSettings.pause_available = true
         end,
 
+        -- 7: Test battle.
+        function(self)
+            --load_field_map_request("startmap", "")
+            console("camera_free false")
+            console("debug_walkmesh false")
+            script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
+            set_party(0, 2, 3)
+            Battle.start(372) -- Single level 2 MP, easy test battle
+            MenuSettings.pause_available = true
+        end,
+    },
 
-        --- Handles button events.
-        --
-        -- Handles directional Up, Down and Enter keys.
-        -- @param button Pressed direction or action name.
-        -- @param event Trigger event. Normally, "Press".
-        on_button = function(self, button, event)
-        if UiContainer.current_menu == "begin" then
-            local cursor = ui_manager:get_widget("BeginMenu.Container.Cursor")
+    --- Handles button events.
+    --
+    -- Handles directional Up, Down and Enter keys.
+    -- @param button Pressed direction or action name.
+    -- @param event Trigger event. Normally, "Press".
+    on_button = function(self, button, event)
+        if self:is_active() then
             if button == Config.ACTIONS.CONFIRM and event == "Press" then
-                if self.position == 1 then -- New game
-                    audio_manager:play_sound("Window")
-                    generate_control_key()
-                    load_field_map_request("md1stin", "")
-                    console("camera_free false")
-                    --console("debug_walkmesh true")
-                    script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
-                    MenuSettings.pause_available = true
-                    elseif self.position == 2 then -- Load, show save menu.
-                        audio_manager:play_sound("Cursor")
-                        UiContainer.SaveMenu.operation = "load"
-                        script:request_end_sync(Script.UI, "SaveMenu", "show", 0)
-                        elseif self.position == 3 then
-                            -- NMKIN_3 START
-                            Data.progress_game = 27
-                            load_field_map_request("nmkin_3", "")
-                            console("camera_free false")
-                            console("debug_walkmesh true")
-                            script:wait(1)
-                            script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
-                            entity_manager:get_entity("Cloud"):set_position(-1.090750, 15.310307, 17.281244)
-                            entity_manager:set_player_entity("Cloud")
-                            script:wait(1)
-                            entity_manager:get_player_entity():set_position(-1.093750, 15.320307, 17.281244)
-                            entity_manager:get_entity("Cloud"):jump_to_position(0.46875, 14.9922, -1, 1, 42) -- Triangle 42, 2 steps.
-                            entity_manager:get_entity("Cloud"):jump_sync()
-                            entity_manager:player_lock(false)
-                            MenuSettings.pause_available = true
-                            -- NMKIN_3 END
-                            -- NMKIN_4 START
-                            --[[Data.progress_game = 14
-                            --load_field_map_request("nmkin_4", "")
-                            map("nmkin_4")
-                            console("camera_free false")
-                            console("debug_walkmesh true")
-                            script:wait(1.5)
-                            script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
-                            entity_manager:get_entity("Cloud"):set_position(-0.820312, 0.500000, 17.281244)
-                            entity_manager:set_player_entity("Cloud")
-                            set_party(0, 1, 4)
-                            script:wait(0.5)
-                            entity_manager:get_player_entity():set_position(-0.820312, 0.500000, 17.281244)
-                            entity_manager:get_entity("Cloud"):jump_to_position(-0.800312, 0.500000, -1, 1) -- Triangle 42, 2 steps.
-                            entity_manager:get_entity("Cloud"):jump_sync()
-                            entity_manager:player_lock(false)
-                            print("TRIANGLE " .. entity_manager:get_entity("Cloud"):get_move_triangle_id())
-                            MenuSettings.pause_available = true]]
-                            -- NMKIN_4 END
-                            elseif self.position == 4 then
-                                script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
-                                console("camera_free true")
-                                console("debug_walkmesh true")
-                                map("test_2")
-                                MenuSettings.pause_available = true
-                                elseif self.position == 5 then
-                                    script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
-                                    console("camera_free true")
-                                    console("debug_entity 2")
-                                    MenuSettings.pause_available = true
-                                    world_map_module:init()
-                                    elseif self.position == 6 then
-                                        load_field_map_request("startmap", "")
-                                        console("camera_free false")
-                                        console("debug_walkmesh false")
-                                        script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
-                                        MenuSettings.pause_available = true
-                                        elseif self.position == 7 then
-                                            --load_field_map_request("startmap", "")
-                                            console("camera_free false")
-                                            console("debug_walkmesh false")
-                                            script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
-                                            set_party(0, 2, 3)
-                                            Battle.start(372) -- Single level 2 MP, easy test battle
-                                            MenuSettings.pause_available = true
-                                            end
-                                            elseif button == Config.CONTROLS.DOWN then
-                                                audio_manager:play_sound("Cursor")
-                                                self.position = self.position + 1
-                                                if self.position > self.position_total then
-                                                    self.position = 1;
-        end
-        cursor:set_default_animation("Position" .. self.position)
-        elseif button == Config.CONTROLS.UP then
-            audio_manager:play_sound("Cursor")
-            self.position = self.position - 1
-            if self.position <= 0 then
-                self.position = self.position_total;
-        end
-        cursor:set_default_animation("Position" .. self.position)
-        end
+                local action = self.actions[self.cursor:index()]
+                if action ~= nil then action(self) end
+            else
+                self.cursor:handle(button, "vertical")
+            end
         end
         return 0
-        end,
+    end,
 
-        --- Opens the save menu.
-        show = function(self)
-        UiContainer.current_menu = "begin"
+    --- Opens the begin menu.
+    show = function(self)
+        self:activate()
         ui_manager:get_widget("BeginMenu"):set_visible(true)
-        return 0;
-        end,
+        return 0
+    end,
 
-        --- Hiddens the begin menu.
-        --
-        -- It doesn't do anything else.
-        hide = function(self)
-        UiContainer.current_menu = ""
+    --- Hides the begin menu.
+    --
+    -- It doesn't do anything else.
+    hide = function(self)
+        self:deactivate("")
         ui_manager:get_widget("BeginMenu"):set_visible(false)
-        return 0;
-        end,
-    }
+        return 0
+    end,
+}

+ 687 - 0
data/data/scripts/menu/config_menu.lua

@@ -0,0 +1,687 @@
+if UiContainer == nil then UiContainer = {} end
+
+local ROOT = "ConfigMenu.Container."
+
+--- Range of the values of the red, green and blue sliders of the window colour.
+local COLOUR_MIN = 0
+local COLOUR_MAX = 255
+
+--- Step of the colour sliders: left and right, and L1 and R1 (big step).
+local COLOUR_STEP = 1
+local COLOUR_BIG_STEP = 16
+
+--- Position and width in pixels of the colour sliders, relative to the colour window.
+local SLIDER_X = 70
+local SLIDER_WIDTH = 92
+
+--- Corners of the window, in the order they are shown (2x2 grid), and the corner of the colours
+-- table (top-left, top-right, bottom-right, bottom-left) each one is.
+local CORNER_COLOUR = { 1, 2, 4, 3 }
+
+--- Actions the player can choose the keys of. The first entry is the button that restores the
+-- default keys.
+--
+--  * name: Name of the row widget (with the prefix "Row", because the window prototype has children
+--    called Left and Right) and of the texts.
+--  * events: Game events (see bind_game_event in config.cfg) that use the keys of the action. The
+--    default keys are the ones config.cfg gives to the first one (see get_default_keys).
+local CONTROLS = {
+    { name = "Default" },
+    { name = "Up", events = { "Up", "walk_up", "message_up" } },
+    { name = "Down", events = { "Down", "walk_down", "message_down" } },
+    { name = "Left", events = { "Left", "walk_left" } },
+    { name = "Right", events = { "Right", "walk_right" } },
+    { name = "Ok", events = { "CONFIRM" } },
+    { name = "Cancel", events = { "CANCEL" } },
+    { name = "Menu", events = { "MENU" } },
+    { name = "Extra", events = { "EXTRA" } },
+    { name = "Select", events = { "SELECT" } },
+    { name = "Start", events = { "START" } },
+    { name = "L1", events = { "L1" } },
+    { name = "R1", events = { "R1" } },
+    { name = "Run", events = { "run" } },
+}
+
+--- File where the keys are saved. The game runs it after config.cfg when it starts.
+local CONTROLS_FILE = "./controls.cfg"
+
+--- Key that cancels the choice of a key. It can't be assigned.
+local CAPTURE_CANCEL_KEY = "Escape"
+
+--- Maximum value of the volume sliders (0 is muted).
+local VOLUME_MAX = 4
+
+--- Range of the sliders. 0 is the left end (fast), SLIDER_MAX the right end (slow).
+local SLIDER_MAX = 10
+
+--- Types of row.
+local KIND = {
+    ACTION = "action",   -- Opens something when OK is pressed.
+    CHOICE = "choice",   -- Left/right choose one of several options.
+    SLIDER = "slider",   -- Left/right move a slider.
+    ORDER = "order",     -- Left/right choose one of the orders of the magic menu.
+}
+
+--- The rows of the menu, in the order they are shown.
+--
+--  * name: Name of the row widget.
+--  * kind: Type of row (KIND). Sound and controller open their own window, like window color.
+--  * key: Field of MenuSettings.config where the value is stored.
+--  * count: Number of options (choices and orders).
+local ROWS = {
+    { name = "WindowColor", kind = KIND.ACTION },
+    { name = "Sound", kind = KIND.ACTION },
+    { name = "Controller", kind = KIND.ACTION },
+    { name = "CursorMode", kind = KIND.CHOICE, key = "cursor", count = 2 },
+    { name = "Atb", kind = KIND.CHOICE, key = "atb", count = 3 },
+    { name = "BattleSpeed", kind = KIND.SLIDER, key = "battle_speed" },
+    { name = "BattleMessage", kind = KIND.SLIDER, key = "battle_message" },
+    { name = "FieldMessage", kind = KIND.SLIDER, key = "field_message" },
+    { name = "Movement", kind = KIND.CHOICE, key = "movement", count = 2 },
+    { name = "CameraAngle", kind = KIND.CHOICE, key = "camera", count = 2 },
+    { name = "MagicOrder", kind = KIND.ORDER, key = "magic_order", count = 6 },
+}
+
+--- Values of the options. Choices and orders start at 1, sliders at 0.
+--
+-- TODO: These are placeholders. They don't change the game yet. Each option must be connected to
+-- its setting as it is implemented.
+-- The settings are saved with the game (save_settings) and loaded with it (load_settings). The keys of
+-- the controller are not: they are saved in controls.cfg.
+MenuSettings.config = MenuSettings.config or {
+    cursor = 1,         -- 1: Initial, 2: Memory.
+    atb = 2,            -- 1: Active, 2: Recommended, 3: Wait.
+    battle_speed = 5,
+    battle_message = 5,
+    field_message = 5,
+    movement = 1,       -- 1: Walk, 2: Run.
+    camera = 1,         -- 1: Auto, 2: Fix.
+    magic_order = 1,    -- Permutations of restore, attack and indirect.
+
+    music_volume = 4,   -- 0-4.
+    sound_volume = 4,   -- 0-4. Sound effects.
+
+    --- Colours of the windows (red, green, blue, 0-255) at the top-left, top-right, bottom-right
+    -- and bottom-left corners. They are the ones the windows have in the XML.
+    window_colours = {
+        { 0, 0, 178 },
+        { 0, 0, 127 },
+        { 0, 0, 32 },
+        { 0, 0, 79 },
+    },
+}
+
+--- Gets the colours of the windows as the 12 numbers (0-1) the engine takes.
+local function window_colours_as_floats()
+    local values = {}
+    for _, colour in ipairs(MenuSettings.config.window_colours) do
+        for channel = 1, 3 do
+            values[#values + 1] = colour[channel] / 255
+        end
+    end
+    return values
+end
+
+--- Splits a list of keys separated by commas.
+local function split_keys(text)
+    local keys = {}
+    for key in string.gmatch(text, "[^,]+") do keys[#keys + 1] = key end
+    return keys
+end
+
+--- Gets the keys of an action of CONTROLS.
+local function get_keys(action)
+    return split_keys(input_manager:get_game_event_keys(action.events[1]))
+end
+
+--- Gets the default keys of an action of CONTROLS: the ones of config.cfg.
+local function get_default_keys(action)
+    return split_keys(input_manager:get_default_game_event_keys(action.events[1]))
+end
+
+--- Sets the keys of an action of CONTROLS, in all its game events.
+local function set_keys(action, keys)
+    local text = table.concat(keys, ",")
+    for _, event in ipairs(action.events) do
+        input_manager:set_game_event_keys(event, text)
+    end
+end
+
+--- Settings (fields of MenuSettings.config) saved with the game, apart from the window colours.
+-- The values are numbers: choices start at 1, and sliders and volumes at 0.
+local SAVED_SETTINGS = {
+    "music_volume", "sound_volume", "cursor", "atb", "battle_speed", "battle_message",
+    "field_message", "movement", "camera", "magic_order",
+}
+
+--- Handlers of the buttons that change the value of the colour under the cursor.
+local function colour_buttons()
+    local buttons = {}
+    buttons[Config.CONTROLS.LEFT] = function(self, event) self:change_colour(-COLOUR_STEP) end
+    buttons[Config.CONTROLS.RIGHT] = function(self, event) self:change_colour(COLOUR_STEP) end
+    buttons[Config.ACTIONS.L1] = function(self, event) self:change_colour(-COLOUR_BIG_STEP) end
+    buttons[Config.ACTIONS.R1] = function(self, event) self:change_colour(COLOUR_BIG_STEP) end
+    return buttons
+end
+
+--- Handlers of Left and Right, which change the value of the row under the cursor.
+local function value_buttons()
+    local buttons = {}
+    buttons[Config.CONTROLS.LEFT] = function(self, event) self:change_value(-1) end
+    buttons[Config.CONTROLS.RIGHT] = function(self, event) self:change_value(1) end
+    return buttons
+end
+
+--- The config menu.
+--
+-- Up and down choose a row, and left and right change its value right away. Window color, sound
+-- and controller open their own window with OK.
+--
+-- Sound: opens a window with two sliders, one for the volume of the music and another for the
+-- sound effects (0-4). The volume changes as soon as the slider moves.
+--
+-- Controller: opens a window with the keys of every action (the directions, OK, cancel...). OK on
+-- an action asks for the key to assign to it. If the key was used by another action, they swap
+-- their keys. The first entry restores the default keys. The changes are kept apart and are only
+-- applied (and saved in controls.cfg, which the game reads when it starts) when the window is
+-- closed, if they are valid (see controls_are_valid), so the player can't be left without a way to
+-- control the game.
+--
+-- Window color: the cursor goes to the preview of the window to the right, where one of the four
+-- corners is chosen. Each corner opens a window with three sliders (red, green and blue). The
+-- preview shows the changes, and the colours are applied to all the windows when the cursor goes
+-- back to the list.
+--
+-- TODO: All the options are placeholders for now.
+UiContainer.ConfigMenu = UiContainer.Menu.new{
+
+    id = "config",
+    parent = "main",
+
+    --- Cursor over the rows.
+    cursor = UiContainer.Cursor.new{
+        widget = ROOT .. "Cursor",
+        total = #ROWS,
+        on_move = function(position)
+            UiContainer.ConfigMenu:update_help(position)
+        end,
+    },
+
+    --- Cursor over the corners of the window preview (2x2 grid).
+    corner_cursor = UiContainer.Cursor.new{
+        widget = ROOT .. "CornerCursor",
+        total = 4,
+        sound = false,
+    },
+
+    --- Cursor over the actions of the controller window.
+    controller_cursor = UiContainer.Cursor.new{
+        widget = ROOT .. "ControllerWindow.Cursor",
+        total = #CONTROLS,
+    },
+
+    --- Action (entry of CONTROLS) that is waiting for a key.
+    assigning = nil,
+
+    --- Keys the player has chosen in the controller window, not applied yet. Lists of key names by
+    -- position in CONTROLS.
+    pending_keys = {},
+
+    --- Cursor over the music and sound effects sliders.
+    sound_cursor = UiContainer.Cursor.new{
+        widget = ROOT .. "SoundWindow.Cursor",
+        total = 2,
+    },
+
+    --- Cursor over the red, green and blue sliders.
+    channel_cursor = UiContainer.Cursor.new{
+        widget = ROOT .. "ColourWindow.Cursor",
+        total = 3,
+    },
+
+    states = {
+        [""] = {
+            cursor = function(self) return self.cursor end,
+            axis = "vertical",
+            buttons = value_buttons(),
+            confirm = function(self, cursor)
+                local row = ROWS[cursor:index()]
+                if row.name == "WindowColor" then
+                    self.corner_cursor:set(1)
+                    ui_manager:get_widget(ROOT .. "CornerCursor"):set_visible(true)
+                    self:set_state("corner")
+                    return
+                elseif row.name == "Sound" then
+                    self.sound_cursor:set(1)
+                    self:refresh_sound_window()
+                    ui_manager:get_widget(ROOT .. "SoundWindow"):set_visible(true)
+                    self:set_state("sound")
+                    return
+                elseif row.name == "Controller" then
+                    self.controller_cursor:set(1)
+                    self:load_pending_keys()
+                    self:refresh_controller_window()
+                    ui_manager:get_widget(ROOT .. "ControllerWindow"):set_visible(true)
+                    self:set_state("controller")
+                    return
+                elseif row.kind == KIND.ACTION then
+                    print("TODO: Open the window of " .. row.name)
+                end
+                -- The other rows change with left and right.
+                return false
+            end,
+            cancel = function(self)
+                script:request_end_sync(Script.UI, "ConfigMenu", "hide", 0)
+            end,
+        },
+
+        --- Choosing the action to change the key of.
+        controller = {
+            cursor = function(self) return self.controller_cursor end,
+            axis = "vertical",
+            confirm = function(self, cursor)
+                local index = cursor:index()
+                if index == 1 then
+                    self:reset_controls()
+                    self:refresh_controller_window()
+                    return
+                end
+                -- The next key pressed is the new key of the action.
+                self.assigning = index
+                input_manager:capture_next_key()
+                self:set_cursor_blinking(true)
+                self:update_help(#ROWS + 1)
+                self:set_state("assign")
+            end,
+            cancel = function(self)
+                -- The keys are only applied if they are valid. If not, the window stays open.
+                if not self:controls_are_valid() then return false end
+                ui_manager:get_widget(ROOT .. "ControllerWindow"):set_visible(false)
+                self:apply_controls()
+                self:set_state("")
+            end,
+        },
+
+        --- Waiting for the key to assign to an action.
+        assign = {
+            capture = function(self, key)
+                if key == "UNASSIGNED" then
+                    -- A key the game doesn't know: ask again.
+                    UiContainer.play_sound("Error")
+                    input_manager:capture_next_key()
+                    return
+                elseif key == CAPTURE_CANCEL_KEY then
+                    UiContainer.play_sound("Back")
+                else
+                    self:assign_key(self.assigning, key)
+                    UiContainer.play_sound("Cursor")
+                    self:refresh_controller_window()
+                end
+                self:set_cursor_blinking(false)
+                self:update_help(self.cursor:index())
+                self:set_state("controller")
+            end,
+        },
+
+        --- Changing the volume of the music and of the sound effects.
+        sound = {
+            cursor = function(self) return self.sound_cursor end,
+            axis = "vertical",
+            buttons = {
+                [Config.CONTROLS.LEFT] = function(self, event) self:change_volume(-1) end,
+                [Config.CONTROLS.RIGHT] = function(self, event) self:change_volume(1) end,
+            },
+            confirm = function(self)
+                ui_manager:get_widget(ROOT .. "SoundWindow"):set_visible(false)
+                self:set_state("")
+            end,
+            cancel = function(self)
+                ui_manager:get_widget(ROOT .. "SoundWindow"):set_visible(false)
+                self:set_state("")
+            end,
+        },
+
+        --- Choosing the corner of the window to change.
+        corner = {
+            buttons = UiContainer.grid_buttons("corner_cursor", 2),
+            confirm = function(self)
+                self.channel_cursor:set(1)
+                self:refresh_colour_window()
+                ui_manager:get_widget(ROOT .. "ColourWindow"):set_visible(true)
+                self:set_state("colour")
+            end,
+            cancel = function(self)
+                -- Back to the list: the colours are applied now.
+                ui_manager:get_widget(ROOT .. "CornerCursor"):set_visible(false)
+                self:apply_window_colours()
+                self:set_state("")
+            end,
+        },
+
+        --- Changing the red, green and blue of a corner.
+        colour = {
+            cursor = function(self) return self.channel_cursor end,
+            axis = "vertical",
+            buttons = colour_buttons(),
+            confirm = function(self)
+                ui_manager:get_widget(ROOT .. "ColourWindow"):set_visible(false)
+                self:set_state("corner")
+            end,
+            cancel = function(self)
+                ui_manager:get_widget(ROOT .. "ColourWindow"):set_visible(false)
+                self:set_state("corner")
+            end,
+        },
+    },
+
+    --- Makes the cursor of the controller window blink (while a key is asked for), or stop.
+    --
+    -- @param blinking True to blink, false to go back to the fixed cursor.
+    set_cursor_blinking = function(self, blinking)
+        local cursor = ui_manager:get_widget(ROOT .. "ControllerWindow.Cursor")
+        local animation = (blinking and "Blink" or "Position") .. tostring(self.controller_cursor:index())
+        cursor:set_default_animation(animation)
+        cursor:play_animation(animation)
+        -- The blink leaves it transparent when it stops at the wrong moment.
+        if not blinking then cursor:set_alpha(1.0) end
+    end,
+
+    --- Shows the keys of every action in the controller window.
+    refresh_controller_window = function(self)
+        for index = 2, #CONTROLS do
+            local widget = ui_manager:get_widget(ROOT .. "ControllerWindow.Row" .. CONTROLS[index].name .. ".Keys")
+            widget:set_colour(1.0, 1.0, 1.0)
+            widget:set_text(table.concat(self.pending_keys[index], ", "))
+        end
+    end,
+
+    --- Copies the keys the actions have now to the pending keys.
+    load_pending_keys = function(self)
+        self.pending_keys = {}
+        for index = 2, #CONTROLS do
+            self.pending_keys[index] = get_keys(CONTROLS[index])
+        end
+    end,
+
+    --- Checks that the pending keys leave the game controllable.
+    --
+    -- Every action must have at least one key, and no key can be used by two actions.
+    --
+    -- @return True if they can be applied.
+    controls_are_valid = function(self)
+        local used = {}
+        for index = 2, #CONTROLS do
+            local keys = self.pending_keys[index]
+            if keys == nil or #keys == 0 then return false end
+            for _, key in ipairs(keys) do
+                if used[key] then return false end
+                used[key] = true
+            end
+        end
+        return true
+    end,
+
+    --- Applies the pending keys to the game and saves them.
+    apply_controls = function(self)
+        for index = 2, #CONTROLS do
+            set_keys(CONTROLS[index], self.pending_keys[index])
+        end
+        self:save_controls()
+    end,
+
+    --- Gives a key to an action, in the pending keys.
+    --
+    -- The action keeps only that key. If another action had it, it loses it and, if it was its
+    -- only key, it takes the keys the action had before (they swap).
+    --
+    -- @param index Position of the action in CONTROLS.
+    -- @param key Name of the key.
+    assign_key = function(self, index, key)
+        local previous = self.pending_keys[index]
+        for other_index = 2, #CONTROLS do
+            if other_index ~= index then
+                local keys = self.pending_keys[other_index]
+                for position, existing in ipairs(keys) do
+                    if existing == key then
+                        table.remove(keys, position)
+                        if #keys == 0 then
+                            keys = (#previous > 0) and previous or get_default_keys(CONTROLS[other_index])
+                        end
+                        self.pending_keys[other_index] = keys
+                        break
+                    end
+                end
+            end
+        end
+        self.pending_keys[index] = { key }
+    end,
+
+    --- Puts the default keys of all the actions in the pending keys.
+    reset_controls = function(self)
+        for index = 2, #CONTROLS do
+            self.pending_keys[index] = get_default_keys(CONTROLS[index])
+        end
+    end,
+
+    --- Saves the keys in the controls file, which the game runs when it starts.
+    save_controls = function(self)
+        local file = io.open(CONTROLS_FILE, "w")
+        if file == nil then
+            print("Can't write the controller settings in " .. CONTROLS_FILE)
+            return
+        end
+        for index = 2, #CONTROLS do
+            local action = CONTROLS[index]
+            local keys = table.concat(self.pending_keys[index], ",")
+            for _, event in ipairs(action.events) do
+                file:write('set_game_event_keys "' .. event .. '" "' .. keys .. '"\n')
+            end
+        end
+        file:close()
+    end,
+
+    --- Changes the volume (music or sound effects, the one under the cursor).
+    --
+    -- @param delta -1 for left, 1 for right.
+    change_volume = function(self, delta)
+        local key = (self.sound_cursor:index() == 1) and "music_volume" or "sound_volume"
+        local values = MenuSettings.config
+        local value = math.max(0, math.min(VOLUME_MAX, values[key] + delta))
+        if value == values[key] then return end
+        values[key] = value
+        self:apply_volumes()
+        self:refresh_sound_window()
+        -- Played after applying the volume, so the sound effect is heard with the new volume.
+        UiContainer.play_sound("Cursor")
+    end,
+
+    --- Gives the settings to the savemap, to be saved with the game. See save_game in save.lua.
+    save_settings = function(self)
+        local config = MenuSettings.config
+        for _, key in ipairs(SAVED_SETTINGS) do
+            savemap_manager:set_setting(key, config[key])
+        end
+        local c = config.window_colours
+        savemap_manager:set_window_colours(
+            c[1][1], c[1][2], c[1][3], c[2][1], c[2][2], c[2][3],
+            c[3][1], c[3][2], c[3][3], c[4][1], c[4][2], c[4][3]
+        )
+    end,
+
+    --- Loads the settings of a saved game and applies them. See load_game in save.lua.
+    --
+    -- Games saved before the settings were saved don't have them, and don't change the ones the
+    -- player has. Values that are out of range (a damaged file) are brought inside it.
+    --
+    -- @param slot Slot of the saved game (0-15).
+    load_settings = function(self, slot)
+        if not savemap_manager:is_slot_setting_set(slot, SAVED_SETTINGS[1]) then return end
+        local config = MenuSettings.config
+        -- Range of the values of each setting.
+        local ranges = {
+            music_volume = { 0, VOLUME_MAX }, sound_volume = { 0, VOLUME_MAX },
+        }
+        for _, row in ipairs(ROWS) do
+            if row.kind == KIND.SLIDER then ranges[row.key] = { 0, SLIDER_MAX }
+            elseif row.key ~= nil then ranges[row.key] = { 1, row.count } end
+        end
+        for _, key in ipairs(SAVED_SETTINGS) do
+            if savemap_manager:is_slot_setting_set(slot, key) then
+                local range = ranges[key]
+                config[key] = math.max(range[1], math.min(range[2], savemap_manager:get_slot_setting(slot, key)))
+            end
+        end
+        -- Corners: top-left, top-right, bottom-right and bottom-left, as in the savemap.
+        for corner = 1, 4 do
+            for channel = 1, 3 do
+                config.window_colours[corner][channel] = math.max(COLOUR_MIN, math.min(COLOUR_MAX,
+                    savemap_manager:get_slot_window_colour(slot, corner - 1, channel - 1)))
+            end
+        end
+        self:apply_all()
+    end,
+
+    --- Applies all the settings that act on the game right away: volumes, movement and the
+    -- colours of the windows. The rest are read by the game when it needs them.
+    apply_all = function(self)
+        self:apply_volumes()
+        self:apply_movement()
+        self:apply_window_colours()
+    end,
+
+    --- Applies the movement setting (walk or run by default) to the player.
+    apply_movement = function(self)
+        entity_manager:set_player_default_run(MenuSettings.config.movement == 2)
+    end,
+
+    --- Applies the volumes to the audio.
+    apply_volumes = function(self)
+        audio_manager:set_music_volume(MenuSettings.config.music_volume / VOLUME_MAX)
+        audio_manager:set_sound_volume(MenuSettings.config.sound_volume / VOLUME_MAX)
+    end,
+
+    --- Shows the position of the volume sliders.
+    refresh_sound_window = function(self)
+        local values = MenuSettings.config
+        ui_manager:get_widget(ROOT .. "SoundWindow.Music.Handle")
+            :set_default_animation("Position" .. tostring(values.music_volume))
+        ui_manager:get_widget(ROOT .. "SoundWindow.Effects.Handle")
+            :set_default_animation("Position" .. tostring(values.sound_volume))
+    end,
+
+    --- Colour (entry of MenuSettings.config.window_colours) of the corner under the cursor.
+    current_colour = function(self)
+        return MenuSettings.config.window_colours[CORNER_COLOUR[self.corner_cursor:index()]]
+    end,
+
+    --- Changes the red, green or blue (the one under the cursor) of the corner.
+    --
+    -- @param delta Amount added to the value.
+    change_colour = function(self, delta)
+        local colour = self:current_colour()
+        local channel = self.channel_cursor:index()
+        local value = math.max(COLOUR_MIN, math.min(COLOUR_MAX, colour[channel] + delta))
+        if value == colour[channel] then return end
+        colour[channel] = value
+        self:refresh_colour_window()
+        self:refresh_preview()
+    end,
+
+    --- Shows the sliders of the corner under the cursor.
+    refresh_colour_window = function(self)
+        local colour = self:current_colour()
+        local names = { "Red", "Green", "Blue" }
+        for channel = 1, 3 do
+            local path = ROOT .. "ColourWindow." .. names[channel]
+            local value = colour[channel]
+            ui_manager:get_widget(path .. ".Value"):set_text(UiContainer.pad_value(value, 3))
+            local offset = math.floor((value - COLOUR_MIN) / (COLOUR_MAX - COLOUR_MIN) * SLIDER_WIDTH)
+            ui_manager:get_widget(path .. ".Handle"):set_x(0, SLIDER_X + offset)
+        end
+    end,
+
+    --- Shows the chosen colours in the preview of the window.
+    refresh_preview = function(self)
+        local v = window_colours_as_floats()
+        ui_manager:get_widget(ROOT .. "Body.WindowColor.Preview.Center"):set_colours(
+            v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8], v[9], v[10], v[11], v[12])
+    end,
+
+    --- Applies the chosen colours to all the windows.
+    apply_window_colours = function(self)
+        local v = window_colours_as_floats()
+        ui_manager:set_window_colours(
+            v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8], v[9], v[10], v[11], v[12])
+    end,
+
+    --- Shows only the help text of a row.
+    update_help = function(self, position)
+        -- There is one more help text than rows: the one shown while a key is asked for.
+        for index = 1, #ROWS + 1 do
+            ui_manager:get_widget(ROOT .. "Help.H" .. tostring(index)):set_visible(index == position)
+        end
+    end,
+
+    --- Changes the value of the row under the cursor.
+    --
+    -- @param delta -1 for left, 1 for right.
+    change_value = function(self, delta)
+        local row = ROWS[self.cursor:index()]
+        if row.kind == KIND.ACTION then return end
+        local values = MenuSettings.config
+        local min = (row.kind == KIND.SLIDER) and 0 or 1
+        local max = (row.kind == KIND.SLIDER) and SLIDER_MAX or row.count
+        local value = values[row.key] + delta
+        if value < min or value > max then return end
+        values[row.key] = value
+        if row.key == "movement" then self:apply_movement() end
+        UiContainer.play_sound("Cursor")
+        self:refresh_row(row)
+    end,
+
+    --- Shows the value of a row.
+    refresh_row = function(self, row)
+        local path = ROOT .. "Body." .. row.name
+        local value = MenuSettings.config[row.key]
+        if row.kind == KIND.CHOICE then
+            -- Every option has a white copy (shown when selected) and a gray one (shown otherwise).
+            -- The gray one uses its own text ("...Off") in the texts file, because the colour
+            -- attribute of the XML is not applied to texts that come from the texts file.
+            for option = 1, row.count do
+                local selected = (option == value)
+                ui_manager:get_widget(path .. ".Opt" .. tostring(option) .. "On"):set_visible(selected)
+                ui_manager:get_widget(path .. ".Opt" .. tostring(option) .. "Off"):set_visible(not selected)
+            end
+        elseif row.kind == KIND.SLIDER then
+            ui_manager:get_widget(path .. ".Handle"):set_default_animation("Position" .. tostring(value))
+        elseif row.kind == KIND.ORDER then
+            ui_manager:get_widget(path .. ".Number"):set_text(tostring(value))
+            for order = 1, row.count do
+                ui_manager:get_widget(path .. ".Order" .. tostring(order)):set_visible(order == value)
+            end
+        end
+    end,
+
+    --- Opens the config menu.
+    show = function(self)
+        self:activate()
+        for _, row in ipairs(ROWS) do
+            if row.kind ~= KIND.ACTION then self:refresh_row(row) end
+        end
+        self.cursor:set(1)
+        ui_manager:get_widget(ROOT .. "CornerCursor"):set_visible(false)
+        ui_manager:get_widget(ROOT .. "ColourWindow"):set_visible(false)
+        ui_manager:get_widget(ROOT .. "SoundWindow"):set_visible(false)
+        ui_manager:get_widget(ROOT .. "ControllerWindow"):set_visible(false)
+        self:refresh_preview()
+        ui_manager:get_widget("ConfigMenu"):set_visible(true)
+        return 0
+    end,
+
+    --- Closes the config menu and goes back to the main menu.
+    hide = function(self)
+        ui_manager:get_widget("ConfigMenu"):set_visible(false)
+        self:deactivate()
+        return 0
+    end,
+}

+ 20 - 13
data/data/scripts/menu/equip_menu.lua

@@ -111,7 +111,7 @@ UiContainer.EquipMenu = {
                 elseif button == Config.CONTROLS.DOWN then
                     audio_manager:play_sound("Cursor")
                     -- Move one position down only if there is a next item
-                    if #(list) <= self.list_item_selected + 1 then
+                    if #(list) <= self.list_item_selected then
                         return 0
                     end
                     self.list_item_selected = self.list_item_selected + 1
@@ -150,11 +150,11 @@ UiContainer.EquipMenu = {
                     -- Equip the seleced item
                     local item_id = nil
                     if self.selecting_slot == Inventory.ITEM_TYPE.WEAPON then
-                        item_id = self.avail_weapons[self.list_item_selected + 1]
+                        item_id = self.avail_weapons[self.list_item_selected]
                     elseif self.selecting_slot == Inventory.ITEM_TYPE.ARMOR then
-                        item_id = self.avail_armors[self.list_item_selected + 1]
+                        item_id = self.avail_armors[self.list_item_selected]
                     elseif self.selecting_slot == Inventory.ITEM_TYPE.ACCESSORY then
-                        item_id = self.avail_accessories[self.list_item_selected + 1]
+                        item_id = self.avail_accessories[self.list_item_selected]
                     end
                     if Characters.equip(self.char_id, item_id) == false then
                         audio_manager:play_sound("Error")
@@ -258,8 +258,8 @@ UiContainer.EquipMenu = {
             return false
         end
         for i = 1, self.list_position_total do
-            if #(list) >= i + self.list_first_visible then
-                local name = Game.Items[list[i + self.list_first_visible]].name
+            if #(list) >= i + self.list_first_visible - 1 then
+                local name = Game.Items[list[i + self.list_first_visible - 1]].name
                 ui_manager:get_widget("EquipMenu.Container.List.Item" .. tostring(i)):set_text(name)
             else
                 ui_manager:get_widget("EquipMenu.Container.List.Item" .. tostring(i)):set_text("")
@@ -274,8 +274,10 @@ UiContainer.EquipMenu = {
         self.avail_weapons = {}
         self.avail_armors = {}
         self.avail_accessories = {}
-        for _, item in ipairs(Inventory) do
-            if item.item ~= nil then
+        -- The inventory can have empty slots in between, so ipairs() can't be used.
+        for slot = 1, MAX_INVENTORY_SLOTS do
+            local item = Inventory[slot]
+            if item ~= nil and item.item ~= nil then
                 local item_type = Game.Items[item.item].type
                 if item_type == Inventory.ITEM_TYPE.WEAPON or item_type == Inventory.ITEM_TYPE.ARMOR or item_type == Inventory.ITEM_TYPE.ACCESSORY then
                     local equip = Game.Items[item.item].users
@@ -338,13 +340,13 @@ UiContainer.EquipMenu = {
         else
             -- Selecting from the list
             if self.selecting_slot == Inventory.ITEM_TYPE.WEAPON then
-                item = Game.Items[self.avail_weapons[self.list_item_selected + 1]]
+                item = Game.Items[self.avail_weapons[self.list_item_selected]]
                 type = Inventory.ITEM_TYPE.WEAPON
             elseif self.selecting_slot == Inventory.ITEM_TYPE.ARMOR then
-                item = Game.Items[self.avail_armors[self.list_item_selected + 1]]
+                item = Game.Items[self.avail_armors[self.list_item_selected]]
                 type = Inventory.ITEM_TYPE.ARMOR
             elseif self.selecting_slot == Inventory.ITEM_TYPE.ACCESSORY then
-                item = Game.Items[self.avail_accessories[self.list_item_selected + 1]]
+                item = Game.Items[self.avail_accessories[self.list_item_selected]]
                 type = Inventory.ITEM_TYPE.ACCESSORY
             end
         end
@@ -360,6 +362,10 @@ UiContainer.EquipMenu = {
             ui_manager:get_widget("EquipMenu.Container.SlotsEmpty"):set_visible(true)
             return 0
         end
+        if item == nil then
+            -- Nothing to show details for (e.g. empty slot).
+            return 0
+        end
         ui_manager:get_widget("EquipMenu.Container.SlotsEmpty"):set_visible(false)
         ui_manager:get_widget("EquipMenu.Container.Slots.Growth0"):set_visible(false)
         ui_manager:get_widget("EquipMenu.Container.Slots.Growth1"):set_visible(false)
@@ -397,10 +403,10 @@ UiContainer.EquipMenu = {
         local item
         local cur_item
         if self.selecting_slot == Inventory.ITEM_TYPE.WEAPON then
-            item = Game.Items[self.avail_weapons[self.list_item_selected + 1]]
+            item = Game.Items[self.avail_weapons[self.list_item_selected]]
             cur_item = Game.Items[Characters[self.char_id].weapon.id]
         elseif self.selecting_slot == Inventory.ITEM_TYPE.ARMOR then
-            item = Game.Items[self.avail_armors[self.list_item_selected + 1]]
+            item = Game.Items[self.avail_armors[self.list_item_selected]]
             cur_item = Game.Items[Characters[self.char_id].armor.id]
         end
         ui_manager:get_widget("EquipMenu.Container.Stats.AtkDif"):set_visible(false)
@@ -508,6 +514,7 @@ UiContainer.EquipMenu = {
         ui_manager:get_widget("EquipMenu"):set_visible(false)
         UiContainer.current_menu = "main"
         UiContainer.current_submenu = ""
+        UiContainer.resume_menu("main")
         return 0;
     end,
 }

+ 24 - 31
data/data/scripts/menu/item_menu.lua

@@ -191,13 +191,9 @@ UiContainer.ItemMenu = {
                     end
                     cursor:set_default_animation("Position" .. self.item_cursor_position)
                     -- If an item is selected, show description
-                    if (Inventory[self.item_cursor_item_selected] ~= nil) then
-                        desc_label:set_text(Game.Items[Inventory[self.item_cursor_item_selected + 1].item].description)
-                    else
-                        desc_label:set_text("")
-                    end
+                    self.show_item_description(self)
                 elseif button == Config.CONTROLS.UP then
-                    if self.item_cursor_item_selected == 0 then
+                    if self.item_cursor_item_selected <= 1 then
                         -- At the very begining, do nothing
                         audio_manager:play_sound("Error")
                         return 0
@@ -214,36 +210,24 @@ UiContainer.ItemMenu = {
                     end
                     cursor:set_default_animation("Position" .. self.item_cursor_position)
                     -- If an item is selected, show description
-                    if (Inventory[self.item_cursor_item_selected] ~= nil) then
-                        desc_label:set_text(Game.Items[Inventory[self.item_cursor_item_selected + 1].item].description)
-                    else
-                        desc_label:set_text("")
-                    end
+                    self.show_item_description(self)
                 elseif button == Config.ACTIONS.R1 then
                     if self.first_item_in_window + self.item_cursor_position_total < self.inventory_size - self.item_cursor_position_total then
                         audio_manager:play_sound("Cursor")
                         self.item_cursor_item_selected = self.item_cursor_item_selected + self.item_cursor_position_total
                         self.first_item_in_window = self.first_item_in_window + self.item_cursor_position_total
                         self.populate_items(self)
-                        if (Inventory[self.item_cursor_item_selected] ~= nil) then
-                            desc_label:set_text(Game.Items[Inventory[self.item_cursor_item_selected + 1].item].description)
-                        else
-                            desc_label:set_text("")
-                        end
+                        self.show_item_description(self)
                     else
                         audio_manager:play_sound("Error")
                     end
                 elseif button == Config.ACTIONS.L1 then
-                    if self.first_item_in_window - self.item_cursor_position_total > 1 then
+                    if self.first_item_in_window - self.item_cursor_position_total >= 0 then
                         audio_manager:play_sound("Cursor")
                         self.item_cursor_item_selected = self.item_cursor_item_selected - self.item_cursor_position_total
                         self.first_item_in_window = self.first_item_in_window - self.item_cursor_position_total
                         self.populate_items(self)
-                        if (Inventory[self.item_cursor_item_selected] ~= nil) then
-                            desc_label:set_text(Game.Items[Inventory[self.item_cursor_item_selected + 1].item].description)
-                        else
-                            desc_label:set_text("")
-                        end
+                        self.show_item_description(self)
                     else
                         audio_manager:play_sound("Error")
                     end
@@ -253,10 +237,11 @@ UiContainer.ItemMenu = {
                     UiContainer.current_submenu = "bar_selection"
                     ui_manager:get_widget("ItemMenu.Container.ItemList.Cursor"):set_visible(false)
                 elseif button == Config.ACTIONS.CONFIRM then
-                    if Game.Items[Inventory[self.item_cursor_item_selected].item].menu == 1 then
+                    local selected = Inventory[self.item_cursor_item_selected]
+                    if selected ~= nil and Game.Items[selected.item].menu == 1 then
                         audio_manager:play_sound("Cursor")
-                        self.item_in_use = Inventory[self.item_cursor_item_selected].item
-                        if (Game.Items[Inventory[self.item_cursor_item_selected - 1].item].target.default_multiple) == 1 then
+                        self.item_in_use = selected.item
+                        if (Game.Items[selected.item].target.default_multiple) == 1 then
                             self.item_in_use_party = true
                             self.submenu_chars(self, true)
                         else
@@ -506,6 +491,17 @@ UiContainer.ItemMenu = {
         ui_manager:get_widget("ItemMenu.Container.ItemOrder.Cursor"):set_default_animation("Position" .. self.order_cursor_position)
     end,
 
+    --- Shows the description of the highlighted inventory item, or clears it if the slot is empty.
+    show_item_description = function(self)
+        local label = ui_manager:get_widget("ItemMenu.Container.ItemMenuSecondBar.ItemText")
+        local entry = Inventory[self.item_cursor_item_selected]
+        if entry ~= nil then
+            label:set_text(Game.Items[entry.item].description)
+        else
+            label:set_text("")
+        end
+    end,
+
     --- Initializes the item selection selection mode.
     submenu_items = function(self)
         UiContainer.current_submenu = "item_selection"
@@ -527,11 +523,7 @@ UiContainer.ItemMenu = {
         end
 
 
-        if (Inventory[0] ~= nil) then
-            ui_manager:get_widget("ItemMenu.Container.ItemMenuSecondBar.ItemText"):set_text(Game.Items[Inventory[0].item].description)
-        else
-            ui_manager:get_widget("ItemMenu.Container.ItemMenuSecondBar.ItemText"):set_text("")
-        end
+        self.show_item_description(self)
     end,
 
     --- Initializes the character selection mode.
@@ -966,7 +958,7 @@ UiContainer.ItemMenu = {
             local w_name = ui_manager:get_widget("ItemMenu.Container.ItemList.Name" .. tostring(i))
             local w_colon = ui_manager:get_widget("ItemMenu.Container.ItemList.Colon" .. tostring(i))
             local w_qty = ui_manager:get_widget("ItemMenu.Container.ItemList.Qty" .. tostring(i))
-            local inventory_index = i + first_item_in_window + 1
+            local inventory_index = i + first_item_in_window
             if Inventory[inventory_index] == nil then
                 w_icon:set_visible(false)
                 w_name:set_visible(false)
@@ -1059,6 +1051,7 @@ UiContainer.ItemMenu = {
         ui_manager:get_widget("ItemMenu"):set_visible(false)
         UiContainer.current_menu = "main"
         UiContainer.current_submenu = ""
+        UiContainer.resume_menu("main")
         return 0;
     end,
 }

+ 235 - 0
data/data/scripts/menu/limit_menu.lua

@@ -0,0 +1,235 @@
+if UiContainer == nil then UiContainer = {} end
+
+local ROOT = "LimitMenu.Container."
+
+--- Inside width of the limit bar.
+local BAR_WIDTH = 54
+
+--- Number of limit levels, and of techniques of each level.
+local LEVELS = 4
+local TECHNIQUES = 2
+
+--- What the player is doing with the levels.
+local MODE = { SET = 1, CHECK = 2 }
+
+--- Colour of the levels with learned techniques, and of the others.
+local COLOUR_ACTIVE = { 1.0, 1.0, 1.0 }
+local COLOUR_INACTIVE = { 0.45, 0.45, 0.35 }
+
+local function widget(path)
+    return ui_manager:get_widget(ROOT .. path)
+end
+
+--- Is a technique of the character learned?
+local function is_learned(character, level, technique)
+    local learned = character.limit.learned[level]
+    return learned ~= nil and learned[technique] == true
+end
+
+--- Gets the definition (name and description) of a technique, or nil if it isn't known.
+local function get_technique(char_id, level, technique)
+    local definition = Game.Characters[char_id]
+    if definition == nil or definition.limits == nil or definition.limits[level] == nil then
+        return nil
+    end
+    local entry = definition.limits[level].techniques[technique]
+    if entry == nil then return nil end
+    return Game.Attacks[entry.command]
+end
+
+--- The limit menu.
+--
+-- On top there are two choices: Set, to choose the limit level of the character, and Check, to see
+-- the techniques learned at each level. Both let the player move over the four levels.
+--  * Set: choosing a level with learned techniques asks for confirmation. Confirming assigns it
+--    and empties the limit bar.
+--  * Check: choosing a level with learned techniques lets the player move between its techniques,
+--    showing their description.
+-- Choosing a level without learned techniques plays the error sound.
+UiContainer.LimitMenu = UiContainer.Menu.new{
+
+    id = "limit",
+    parent = "main",
+
+    --- ID of the character being shown.
+    char_id = -1,
+
+    --- Set or check (MODE).
+    mode = MODE.SET,
+
+    --- Cursor over Set and Check.
+    mode_cursor = UiContainer.Cursor.new{
+        widget = ROOT .. "Character.Modes.Cursor",
+        total = 2,
+    },
+
+    --- Cursor over the four levels, in a 2x2 grid.
+    level_cursor = UiContainer.Cursor.new{
+        widget = ROOT .. "Body.LevelCursor",
+        total = 4,
+        sound = false,
+    },
+
+    --- Cursor over the techniques of the chosen level.
+    tech_cursor = UiContainer.Cursor.new{
+        total = 2,
+        on_move = function(position)
+            UiContainer.LimitMenu:update_technique(position)
+        end,
+    },
+
+    --- Cursor over Yes and No in the confirmation window.
+    confirm_cursor = UiContainer.Cursor.new{
+        widget = ROOT .. "Confirm.Cursor",
+        total = 2,
+    },
+
+    states = {
+        --- Choosing Set or Check.
+        [""] = {
+            cursor = function(self) return self.mode_cursor end,
+            axis = "horizontal",
+            confirm = function(self, cursor)
+                self.mode = cursor:index()
+                self.level_cursor:set(1)
+                widget("Body.LevelCursor"):set_visible(true)
+                self:set_state("level")
+            end,
+            cancel = function(self)
+                script:request_end_sync(Script.UI, "LimitMenu", "hide", 0)
+            end,
+        },
+
+        --- Choosing a level.
+        level = {
+            buttons = UiContainer.grid_buttons("level_cursor", 2),
+            confirm = function(self)
+                local level = self.level_cursor:index()
+                if not self:has_learned(level) then return false end
+                if self.mode == MODE.SET then
+                    self.confirm_cursor:set(2) -- No.
+                    widget("Confirm"):set_visible(true)
+                    self:set_state("confirm")
+                else
+                    widget("Body.LevelCursor"):set_visible(false)
+                    widget("Body.TechCursor"):set_visible(true)
+                    self.tech_cursor:set(1)
+                    self:set_state("technique")
+                end
+            end,
+            cancel = function(self)
+                widget("Body.LevelCursor"):set_visible(false)
+                self:set_state("")
+            end,
+        },
+
+        --- Moving between the techniques of a level.
+        technique = {
+            cursor = function(self) return self.tech_cursor end,
+            axis = "vertical",
+            cancel = function(self)
+                widget("Body.TechCursor"):set_visible(false)
+                widget("Help.Help"):set_text("")
+                widget("Body.LevelCursor"):set_visible(true)
+                self:set_state("level")
+            end,
+        },
+
+        --- Confirming the change of level.
+        confirm = {
+            cursor = function(self) return self.confirm_cursor end,
+            axis = "vertical",
+            confirm = function(self, cursor)
+                if cursor:index() == 1 then
+                    self:set_level(self.level_cursor:index())
+                end
+                widget("Confirm"):set_visible(false)
+                self:set_state("level")
+            end,
+            cancel = function(self)
+                widget("Confirm"):set_visible(false)
+                self:set_state("level")
+            end,
+        },
+    },
+
+    --- Does the character know any technique of a level?
+    has_learned = function(self, level)
+        local character = Characters[self.char_id]
+        for technique = 1, TECHNIQUES do
+            if is_learned(character, level, technique) then return true end
+        end
+        return false
+    end,
+
+    --- Assigns a limit level to the character and empties its limit bar.
+    set_level = function(self, level)
+        local character = Characters[self.char_id]
+        character.limit.current = level
+        character.limit.bar = 0
+        self:populate_limit(character)
+    end,
+
+    --- Moves the technique cursor and shows the description of the technique.
+    update_technique = function(self, position)
+        local level = self.level_cursor:index()
+        widget("Body.TechCursor"):set_default_animation("Position" .. tostring((level - 1) * 2 + position))
+        local description = ""
+        if is_learned(Characters[self.char_id], level, position) then
+            local attack = get_technique(self.char_id, level, position)
+            if attack ~= nil then description = attack.description end
+        end
+        widget("Help.Help"):set_text(description)
+    end,
+
+    --- Shows the limit level and bar of the character.
+    populate_limit = function(self, character)
+        widget("Character.LimitLevel"):set_text(tostring(character.limit.current))
+        UiContainer.set_limit_bar(ROOT .. "Character.LimitLinePart", character, BAR_WIDTH)
+    end,
+
+    --- Shows the names of the levels and of their learned techniques.
+    populate = function(self)
+        local character = Characters[self.char_id]
+        UiContainer.populate_character_data(ROOT .. "Character", character)
+        widget("Character.Portrait"):set_image("characters/" .. tostring(self.char_id) .. ".png")
+        self:populate_limit(character)
+
+        for level = 1, LEVELS do
+            local learned = self:has_learned(level)
+            local colour = learned and COLOUR_ACTIVE or COLOUR_INACTIVE
+            local label = widget("Body.Level" .. tostring(level) .. ".Lbl")
+            label:set_colour(colour[1], colour[2], colour[3])
+            label:set_text("LEVEL " .. tostring(level))
+            for technique = 1, TECHNIQUES do
+                local name = ""
+                if is_learned(character, level, technique) then
+                    local attack = get_technique(self.char_id, level, technique)
+                    if attack ~= nil then name = attack.name end
+                end
+                widget("Body.Level" .. tostring(level) .. ".Tech" .. tostring(technique)):set_text(name)
+            end
+        end
+        widget("Help.Help"):set_text("")
+    end,
+
+    --- Opens the limit menu for UiContainer.current_character_id.
+    show = function(self)
+        self.char_id = UiContainer.current_character_id
+        self:activate()
+        self:populate()
+        self.mode_cursor:set(1)
+        widget("Body.LevelCursor"):set_visible(false)
+        widget("Body.TechCursor"):set_visible(false)
+        widget("Confirm"):set_visible(false)
+        ui_manager:get_widget("LimitMenu"):set_visible(true)
+        return 0
+    end,
+
+    --- Hides the limit menu and goes back to the main menu.
+    hide = function(self)
+        ui_manager:get_widget("LimitMenu"):set_visible(false)
+        self:deactivate()
+        return 0
+    end,
+}

+ 393 - 0
data/data/scripts/menu/magic_menu.lua

@@ -0,0 +1,393 @@
+if UiContainer == nil then UiContainer = {} end
+
+local ROOT = "MagicMenu.Container."
+
+--- Rows visible in the list, and abilities in each row.
+local ROWS = 9
+local COLUMNS = 4
+
+--- Categories of the abilities, in the order of the options of the menu.
+local KINDS = { "magic", "summon", "e_skill" }
+
+--- Colour of the abilities that can be used from the menu, and of the rest.
+local COLOUR_USABLE = { 1.0, 1.0, 1.0 }
+local COLOUR_UNUSABLE = { 0.45, 0.45, 0.35 }
+
+--- Number of uses that means a summon can be used without limit.
+local UNLIMITED = 255
+
+--- Attacks (by ID) that can be used from the menu, out of battle. The rest are shown in gray.
+--
+-- The data of the attacks has a condition for the menu, but it is not reliable (Life has none, and
+-- some spells that can't be cast out of battle have one), so the list is explicit.
+local MENU_ATTACKS = {
+    [0] = true,  -- Cure
+    [1] = true,  -- Cure2
+    [2] = true,  -- Cure3
+    [7] = true,  -- Life
+    [8] = true,  -- Life2
+}
+
+--- Creates the entry of the list for an attack.
+--
+-- @param id ID of the attack.
+-- @param uses Times it can be used (summons), or nil.
+-- @param mp MP the character has now. The attacks that cost more are not usable.
+local function create_entry(id, uses, mp)
+    local attack = Game.Attacks[id]
+    if attack == nil then return nil end
+    return {
+        id = id,
+        name = attack.name,
+        description = attack.description or "",
+        mp = attack.mp or 0,
+        uses = uses,
+        usable = MENU_ATTACKS[id] == true and (attack.mp or 0) <= mp,
+    }
+end
+
+--- Splits a list in rows of at most COLUMNS elements.
+local function split_in_rows(list)
+    local rows = {}
+    for index, item in ipairs(list) do
+        local row = math.floor((index - 1) / COLUMNS) + 1
+        rows[row] = rows[row] or {}
+        rows[row][#rows[row] + 1] = item
+    end
+    return rows
+end
+
+--- The magic menu.
+--
+-- At first the player chooses what to see (magic, summons or enemy skills) in the box at the right,
+-- and the abilities of that kind are shown below in a grid of four columns. Magic has a row for each
+-- materia, with the spells it gives. Summons and enemy skills fill the rows in order. OK goes into
+-- the grid, where the cursor moves over the abilities. The description of the one that is highlighted is shown in the help
+-- window, and the MP it needs (and the times it can be used, for summons) in the box at the right.
+--
+-- The abilities that can be cast from the menu (MENU_ATTACKS) are white, as long as the character
+-- has the MP they need. The rest are gray.
+--
+-- OK on one of them opens a window with the party to choose who receives it. The spell heals or
+-- revives and the MP are spent, or it fails (Error sound) and nothing is spent.
+-- TODO: Added-Ability is always "Nothing." The ability linked to the materia isn't calculated.
+UiContainer.MagicMenu = UiContainer.Menu.new{
+
+    id = "magic",
+    parent = "main",
+
+    --- ID of the character being shown.
+    char_id = -1,
+
+    --- Rows of the grid of the character, by kind (magic, summon, e_skill). Each row is a list of
+    -- up to COLUMNS entries.
+    rows = { magic = {}, summon = {}, e_skill = {} },
+
+    --- Kind of abilities whose grid is shown.
+    kind = "magic",
+
+    --- Position of the cursor in the grid: row and column of the highlighted ability, and the first
+    -- row that is visible.
+    row = 1,
+    column = 1,
+    first_row = 1,
+
+    --- Cursor over the options: magic, summon and enemy skills.
+    category_cursor = UiContainer.Cursor.new{
+        widget = ROOT .. "Side.Options.Cursor",
+        total = #KINDS,
+        on_move = function(position)
+            UiContainer.MagicMenu:show_category(position)
+        end,
+    },
+
+    --- Party slots shown in the window to choose the target of a spell, by position of the cursor.
+    target_slots = {},
+
+    --- Cursor over the characters of the party, to choose who receives the spell.
+    target_cursor = UiContainer.Cursor.new{
+        total = 1,
+        on_move = function(position)
+            UiContainer.MagicMenu:update_target_cursor(position)
+        end,
+    },
+
+    states = {
+        --- Choosing what to see.
+        [""] = {
+            cursor = function(self) return self.category_cursor end,
+            axis = "vertical",
+            confirm = function(self)
+                if #self.rows[self.kind] == 0 then return false end
+                self:set_browsing(true)
+                self.row, self.column, self.first_row = 1, 1, 1
+                self:update_cursor()
+                self:set_state("list")
+            end,
+            cancel = function(self)
+                script:request_end_sync(Script.UI, "MagicMenu", "hide", 0)
+            end,
+        },
+
+        --- Moving over the abilities of the list.
+        list = {
+            confirm = function(self)
+                local entry = self.rows[self.kind][self.row][self.column]
+                if not entry.usable then return false end
+                self:open_targets()
+                self:set_state("target")
+            end,
+            buttons = {
+                [Config.CONTROLS.UP] = function(self, event) self:move_cursor(-1, 0) end,
+                [Config.CONTROLS.DOWN] = function(self, event) self:move_cursor(1, 0) end,
+                [Config.CONTROLS.LEFT] = function(self, event) self:move_cursor(0, -1) end,
+                [Config.CONTROLS.RIGHT] = function(self, event) self:move_cursor(0, 1) end,
+            },
+            cancel = function(self)
+                self:set_browsing(false)
+                self:set_state("")
+            end,
+        },
+
+        --- Choosing the character that receives the spell.
+        target = {
+            cursor = function(self) return self.target_cursor end,
+            axis = "vertical",
+            confirm = function(self, cursor)
+                return self:cast_spell(cursor:index())
+            end,
+            cancel = function(self)
+                self:close_targets()
+                self:set_state("list")
+            end,
+        },
+    },
+
+    --- Opens the window to choose the character that receives the spell.
+    open_targets = function(self)
+        self.target_slots = {}
+        for slot = 1, Party.MAX do
+            if Party[slot] ~= nil and Characters[Party[slot]] ~= nil then
+                self.target_slots[#self.target_slots + 1] = slot
+            end
+        end
+        self.target_cursor.total = #self.target_slots
+        self:populate_targets()
+        ui_manager:get_widget(ROOT .. "Targets"):set_visible(true)
+        self.target_cursor:set(1)
+    end,
+
+    --- Closes the window to choose the target.
+    close_targets = function(self)
+        ui_manager:get_widget(ROOT .. "Targets"):set_visible(false)
+    end,
+
+    --- Shows the data of the party in the window to choose the target.
+    populate_targets = function(self)
+        for slot = 1, Party.MAX do
+            local path = ROOT .. "Targets.Slot" .. tostring(slot)
+            local id = Party[slot]
+            if id ~= nil and Characters[id] ~= nil then
+                UiContainer.populate_character_data(path, Characters[id])
+                ui_manager:get_widget(path .. ".Portrait"):set_image("characters/" .. tostring(id) .. ".png")
+            else
+                UiContainer.populate_character_data(path, nil)
+            end
+        end
+    end,
+
+    --- Puts the cursor of the targets on a character.
+    -- @param position Position in target_slots.
+    update_target_cursor = function(self, position)
+        local slot = self.target_slots[position]
+        if slot ~= nil then
+            ui_manager:get_widget(ROOT .. "Targets.Cursor"):set_default_animation("Position" .. tostring(slot))
+        end
+    end,
+
+    --- Casts the highlighted spell on a character of the party.
+    --
+    -- @param position Position of the target in target_slots.
+    -- @return False if the spell can't be cast (Error sound).
+    cast_spell = function(self, position)
+        local entry = self.rows[self.kind][self.row][self.column]
+        local target = Party[self.target_slots[position]]
+        if not Characters.cast_menu_spell(self.char_id, entry.id, target) then return false end
+
+        -- HP and MP changed.
+        self:populate_targets()
+        UiContainer.populate_character_data(ROOT .. "Character", Characters[self.char_id])
+        self:refresh_usable()
+        -- Without MP for another one, go back to the list.
+        if not entry.usable then
+            self:close_targets()
+            self:set_state("list")
+        end
+    end,
+
+    --- Updates which spells can be cast, as the MP of the character changed.
+    refresh_usable = function(self)
+        local mp = Characters[self.char_id].stats.mp.current
+        for _, rows in pairs(self.rows) do
+            for _, row in ipairs(rows) do
+                for _, entry in ipairs(row) do
+                    entry.usable = MENU_ATTACKS[entry.id] == true and entry.mp <= mp
+                end
+            end
+        end
+        self:populate_list()
+    end,
+
+    --- Builds the grids of abilities of the character.
+    build_rows = function(self)
+        local abilities = Characters.get_abilities(self.char_id)
+        local mp = Characters[self.char_id].stats.mp.current
+        local rows = { magic = {}, summon = {}, e_skill = {} }
+        -- Magic: one row for each materia (more rows if it gives more than COLUMNS spells).
+        for _, group in ipairs(abilities.magic_groups) do
+            local entries = {}
+            for _, id in ipairs(group) do entries[#entries + 1] = create_entry(id, nil, mp) end
+            for _, row in ipairs(split_in_rows(entries)) do rows.magic[#rows.magic + 1] = row end
+        end
+        local summons = {}
+        for _, summon in ipairs(abilities.summon) do
+            summons[#summons + 1] = create_entry(summon.id, summon.times, mp)
+        end
+        rows.summon = split_in_rows(summons)
+        local e_skills = {}
+        for _, id in ipairs(abilities.e_skill) do e_skills[#e_skills + 1] = create_entry(id, nil, mp) end
+        rows.e_skill = split_in_rows(e_skills)
+        self.rows = rows
+    end,
+
+    --- Shows the grid of a kind of abilities, with the cursor out of it.
+    --
+    -- @param position Position of the option (1 magic, 2 summon, 3 enemy skill).
+    show_category = function(self, position)
+        self.kind = KINDS[position]
+        self.row, self.column, self.first_row = 1, 1, 1
+        self:populate_list()
+        self:update_scroll()
+        ui_manager:get_widget(ROOT .. "Help.Help"):set_text("")
+    end,
+
+    --- Shows the abilities of the rows that are visible.
+    populate_list = function(self)
+        local rows = self.rows[self.kind]
+        for visible_row = 1, ROWS do
+            local row = rows[self.first_row + visible_row - 1]
+            for column = 1, COLUMNS do
+                local widget = ui_manager:get_widget(
+                    ROOT .. "List.R" .. tostring(visible_row) .. "C" .. tostring(column))
+                local entry = row ~= nil and row[column] or nil
+                if entry == nil then
+                    widget:set_visible(false)
+                else
+                    -- The colour has to be set before the text, as it applies to it when it's set.
+                    local colour = entry.usable and COLOUR_USABLE or COLOUR_UNUSABLE
+                    widget:set_colour(colour[1], colour[2], colour[3])
+                    widget:set_text(entry.name)
+                    widget:set_visible(true)
+                end
+            end
+        end
+    end,
+
+    --- Moves the scroll bar of the list. It is only shown if the grid doesn't fit.
+    update_scroll = function(self)
+        local count = #self.rows[self.kind]
+        local visible = count > ROWS
+        ui_manager:get_widget(ROOT .. "List.ScrollBg"):set_visible(visible)
+        local scroll = ui_manager:get_widget(ROOT .. "List.Scroll")
+        scroll:set_visible(visible)
+        if visible then
+            local fraction = (self.first_row - 1) / (count - ROWS)
+            -- Between 3 pixels from the top and 21 from the bottom of the list.
+            scroll:set_y(fraction * 100, 3 - fraction * 24)
+        end
+    end,
+
+    --- Moves the cursor over the grid.
+    --
+    -- Up and down keep the column, or go to the last ability if the row has fewer.
+    --
+    -- @param delta_row -1 to go up, 1 to go down.
+    -- @param delta_column -1 to go left, 1 to go right.
+    move_cursor = function(self, delta_row, delta_column)
+        local rows = self.rows[self.kind]
+        local row, column = self.row, self.column + delta_column
+        if delta_column ~= 0 then
+            -- Left and right stay in the row.
+            if column < 1 or column > #rows[row] then return end
+        else
+            row = row + delta_row
+            if row < 1 or row > #rows then
+                UiContainer.play_sound("Error")
+                return
+            end
+            column = math.min(column, #rows[row])
+        end
+        self.row, self.column = row, column
+        UiContainer.play_sound("Cursor")
+        -- Scroll if the cursor leaves the visible rows.
+        local first_row = self.first_row
+        if row < first_row then first_row = row
+        elseif row > first_row + ROWS - 1 then first_row = row - ROWS + 1 end
+        if first_row ~= self.first_row then
+            self.first_row = first_row
+            self:populate_list()
+            self:update_scroll()
+        end
+        self:update_cursor()
+    end,
+
+    --- Puts the cursor on the highlighted ability and shows its description and cost.
+    update_cursor = function(self)
+        local visible_row = self.row - self.first_row + 1
+        ui_manager:get_widget(ROOT .. "List.Cursor"):set_default_animation(
+            "Position" .. tostring(visible_row) .. "-" .. tostring(self.column))
+        local entry = self.rows[self.kind][self.row][self.column]
+        ui_manager:get_widget(ROOT .. "Help.Help"):set_text(entry.description)
+        ui_manager:get_widget(ROOT .. "Side.Cost.MpValue"):set_text(UiContainer.zero_pad_value(entry.mp, 3))
+        local uses = ui_manager:get_widget(ROOT .. "Side.Cost.Uses")
+        uses:set_visible(entry.uses ~= nil)
+        if entry.uses ~= nil then
+            local text = (entry.uses >= UNLIMITED) and "---" or UiContainer.zero_pad_value(entry.uses, 3)
+            ui_manager:get_widget(ROOT .. "Side.Cost.Uses.Value"):set_text(text)
+        end
+    end,
+
+    --- Switches between choosing what to see and moving over the grid.
+    --
+    -- @param browsing True when the cursor is in the grid.
+    set_browsing = function(self, browsing)
+        ui_manager:get_widget(ROOT .. "Side.Options"):set_visible(not browsing)
+        ui_manager:get_widget(ROOT .. "Side.Cost"):set_visible(browsing)
+        ui_manager:get_widget(ROOT .. "Character.Added"):set_visible(browsing)
+        ui_manager:get_widget(ROOT .. "List.Cursor"):set_visible(browsing)
+        if not browsing then
+            ui_manager:get_widget(ROOT .. "Help.Help"):set_text("")
+        end
+    end,
+
+    --- Opens the magic menu for UiContainer.current_character_id.
+    show = function(self)
+        self.char_id = UiContainer.current_character_id
+        self:activate()
+        self:build_rows()
+        local character = Characters[self.char_id]
+        UiContainer.populate_character_data(ROOT .. "Character", character)
+        ui_manager:get_widget(ROOT .. "Character.Portrait"):set_image("characters/" .. tostring(self.char_id) .. ".png")
+        self:set_browsing(false)
+        self.category_cursor:set(1)
+        ui_manager:get_widget("MagicMenu"):set_visible(true)
+        return 0
+    end,
+
+    --- Closes the magic menu and goes back to the main menu.
+    hide = function(self)
+        ui_manager:get_widget("MagicMenu"):set_visible(false)
+        self:deactivate()
+        return 0
+    end,
+}

+ 182 - 161
data/data/scripts/menu/main_menu.lua

@@ -1,205 +1,226 @@
 if UiContainer == nil then UiContainer = {} end
 
+--- Entries of the main menu, by cursor position.
+local ENTRY = {
+    ITEM = 1, MAGIC = 2, MATERIA = 3, EQUIP = 4, STATUS = 5, ORDER = 6, LIMIT = 7,
+    CONFIG = 8, PHS = 9, SAVE = 10, QUIT = 11,
+}
+
+--- Width in pixels of the inside of the level and limit bars.
+local BAR_WIDTH = 62
+
+--- Returned by a character action to keep the character selection open.
+local STAY = "stay"
+
+local CHARACTERS_CURSOR = "MainMenu.Container.Characters.Cursor"
+
 --- The main menu.
-UiContainer.MainMenu = {
+UiContainer.MainMenu = UiContainer.Menu.new{
+
+    id = "main",
 
-    --- Current cursor position in the main menu (1-10).
-    position = 1,
+    --- Cursor over the menu entries (1-11).
+    cursor = UiContainer.Cursor.new{
+        widget = "MainMenu.Container.Menu.Cursor",
+        total = 11,
+    },
 
-    -- Max cursor position in the main menu.
-    position_total = 11,
+    --- Cursor over the party slots (1-3).
+    -- TODO: Skip empty character slots.
+    character_cursor = UiContainer.Cursor.new{
+        widget = CHARACTERS_CURSOR,
+        total = 3,
+    },
 
-    --- When selecting a character, the menu in this position will be opened.
+    --- When selecting a character, the menu entry that will be opened.
     --
     -- If 0, not selecting character.
     select_character_for_menu = 0,
 
-    --- Current cursorposition in the charaster list (1-3).
-    character_position = 1,
+    --- Entries that don't need a character. Return false to signal they can't be opened.
+    entry_actions = {
+        [ENTRY.ITEM] = function(self)
+            script:request_end_sync(Script.UI, "ItemMenu", "show", 0)
+        end,
+        [ENTRY.CONFIG] = function(self)
+            script:request_end_sync(Script.UI, "ConfigMenu", "show", 0)
+        end,
+        [ENTRY.PHS] = function(self)
+            script:request_end_sync(Script.UI, "PhsMenu", "show", 0)
+        end,
+        [ENTRY.SAVE] = function(self)
+            UiContainer.SaveMenu.operation = "save"
+            script:request_end_sync(Script.UI, "SaveMenu", "show", 0)
+        end,
+        [ENTRY.QUIT] = function(self)
+            script:request_end_sync(Script.UI, "QuitMenu", "show", 0)
+        end,
+    },
+
+    --- Entries that need a character. Receive the party slot (1-3).
+    --
+    -- Return false if the entry can't be opened (error sound), or STAY to keep the character
+    -- selection open instead of handing the control to another menu.
+    character_actions = {
+        [ENTRY.MAGIC] = function(self, slot)
+            UiContainer.current_character_id = Party[slot]
+            script:request_end_sync(Script.UI, "MagicMenu", "show", 0)
+        end,
+        [ENTRY.MATERIA] = function(self, slot)
+            UiContainer.current_character_id = Party[slot]
+            script:request_end_sync(Script.UI, "MateriaMenu", "show", 0)
+        end,
+        [ENTRY.EQUIP] = function(self, slot)
+            UiContainer.current_character_id = Party[slot]
+            script:request_end_sync(Script.UI, "EquipMenu", "show", 0)
+        end,
+        [ENTRY.STATUS] = function(self, slot)
+            UiContainer.current_character_id = Party[slot]
+            script:request_end_sync(Script.UI, "StatusMenu", "show", 0)
+        end,
+        --- Toggles the row (front/back) of the character. Stays in the character selection.
+        [ENTRY.ORDER] = function(self, slot)
+            local character = Characters[Party[slot]]
+            character.back_row = not character.back_row
+            self:update_portrait(slot)
+            return STAY
+        end,
+        [ENTRY.LIMIT] = function(self, slot)
+            UiContainer.current_character_id = Party[slot]
+            script:request_end_sync(Script.UI, "LimitMenu", "show", 0)
+        end,
+    },
+
+    states = {
+        --- Choosing an entry of the menu.
+        [""] = {
+            cursor = function(self) return self.cursor end,
+            axis = "vertical",
+            cancel = function(self)
+                script:request_end_sync(Script.UI, "MainMenu", "hide", 0)
+            end,
+            confirm = function(self, cursor)
+                local position = cursor:index()
+                local action = self.entry_actions[position]
+                if action ~= nil then
+                    return action(self)
+                end
+                -- Any other entry needs a character.
+                self.select_character_for_menu = position
+                self:set_state("main_character")
+                ui_manager:get_widget(CHARACTERS_CURSOR):set_visible(true)
+            end,
+        },
+
+        --- Choosing the character to open the entry for.
+        main_character = {
+            cursor = function(self) return self.character_cursor end,
+            axis = "vertical",
+            cancel = function(self)
+                self:set_state("")
+                ui_manager:get_widget(CHARACTERS_CURSOR):set_visible(false)
+            end,
+            confirm = function(self, cursor)
+                local slot = cursor:index()
+                local action = self.character_actions[self.select_character_for_menu]
+                if action == nil or Party[slot] == nil or Characters[Party[slot]] == nil then
+                    return false
+                end
+                local result = action(self, slot)
+                if result == false or result == STAY then
+                    return result
+                end
+                -- The chosen menu takes over. When it closes, the main menu is back at the entry
+                -- list, so the character cursor must not stay behind.
+                self.select_character_for_menu = 0
+                self:set_state("")
+                ui_manager:get_widget(CHARACTERS_CURSOR):set_visible(false)
+            end,
+        },
+    },
+
+    --- Handles the menu button while the main menu is closed.
+    on_inactive_button = function(self, button, event)
+        if ui_manager:get_widget("MainMenu"):is_visible() == false
+            and MenuSettings.available == true
+            and button == Config.ACTIONS.MENU and event == "Press"
+        then
+            UiContainer.play_sound("Back")
+            script:request_end_sync(Script.UI, "MainMenu", "show", 0)
+        end
+    end,
 
-    --- Max cursor position in the characetr list.
-    character_position_total = 3,
+    --- Moves the portrait of a party slot according to the row: back row is 20 px to the right.
+    update_portrait = function(self, slot)
+        local character = Characters[Party[slot]]
+        local portrait = ui_manager:get_widget("MainMenu.Container.Characters.Character" .. tostring(slot) .. ".Portrait")
+        portrait:set_default_animation(character.back_row and "RowBack" or "RowFront")
+    end,
 
-    --- Run when the menu is creaded.
-    --
-    -- It does nothing.
-    on_start = function(self)
-        return 0
+    --- Shows the data of one party slot.
+    populate_character = function(self, slot)
+        local path = "MainMenu.Container.Characters.Character" .. tostring(slot)
+        local character = Party[slot] ~= nil and Characters[Party[slot]] or nil
+        UiContainer.populate_character_data(path, character)
+        if character == nil then return end
+        ui_manager:get_widget(path .. ".Portrait"):set_image("characters/" .. tostring(Party[slot]) .. ".png")
+        self:update_portrait(slot)
+        ui_manager:get_widget(path .. ".LimitLevel"):set_text(tostring(character.limit.current))
+        UiContainer.set_limit_bar(path .. ".LimitLinePart", character, BAR_WIDTH)
+        UiContainer.set_bar(path .. ".NextLevelLinePart", UiContainer.exp_progress(character), BAR_WIDTH)
     end,
 
-    --- Handles button events.
+    --- Refreshes the data a submenu may have changed (level, HP/MP, limit, row, gil...).
     --
-    -- For the current submenu, handles directional keys, enter and escape events.
-    -- @param button Pressed direction or action name.
-    -- @param event Trigger event. Normally, "Press".
-    on_button = function(self, button, event)
-        if UiContainer.current_menu == "main" then
-            local characters  = ui_manager:get_widget("MainMenu.Container.Characters")
-            local menu  = ui_manager:get_widget("MainMenu.Container.Menu")
-            local menu_cursor = ui_manager:get_widget("MainMenu.Container.Menu.Cursor")
-            local timegil = ui_manager:get_widget("MainMenu.Container.TimeGil")
-            local location = ui_manager:get_widget("MainMenu.Container.Location")
-            if UiContainer.current_submenu == "" then
-                if button == Config.ACTIONS.CANCEL and event == "Press" then
-                    audio_manager:play_sound("Back")
-                    script:request_end_sync(Script.UI, "MainMenu", "hide", 0)
-                elseif button == Config.ACTIONS.CONFIRM and event == "Press" then
-                    if self.position == 1 then -- Item menu
-                        audio_manager:play_sound("Cursor")
-                        script:request_end_sync(Script.UI, "ItemMenu", "show", 0)
-                    elseif self.position == 8 then -- Config menu
-                        audio_manager:play_sound("Error")
-                        print("TODO: Open config menu")
-                    elseif self.position == 9 then -- PHS menu
-                        audio_manager:play_sound("Error")
-                        print("TODO: Open PHS menu")
-                    elseif self.position == 10 then -- Save menu
-                        audio_manager:play_sound("Cursor")
-                        UiContainer.SaveMenu.operation = "save"
-                        script:request_end_sync(Script.UI, "SaveMenu", "show", 0)
-                    else -- Any other menu that needs a character
-                        audio_manager:play_sound("Cursor")
-                        self.select_character_for_menu = self.position
-                        UiContainer.current_submenu = "main_character"
-                        ui_manager:get_widget("MainMenu.Container.Characters.Cursor"):set_visible(true)
-                    end
-                elseif button == Config.CONTROLS.DOWN then
-                    audio_manager:play_sound("Cursor")
-                    self.position = self.position + 1
-                    if self.position > self.position_total then
-                        self.position = 1;
-                    end
-                    menu_cursor:set_default_animation("Position" .. self.position)
-                elseif button == Config.CONTROLS.UP then
-                    audio_manager:play_sound("Cursor")
-                    self.position = self.position - 1
-                    if self.position <= 0 then
-                        self.position = self.position_total;
-                    end
-                    menu_cursor:set_default_animation("Position" .. self.position)
-                end
-            elseif UiContainer.current_submenu == "main_character" then
-                if button == Config.ACTIONS.CANCEL and event == "Press" then
-                    audio_manager:play_sound("Back")
-                    UiContainer.current_submenu = ""
-                    ui_manager:get_widget("MainMenu.Container.Characters.Cursor"):set_visible(false)
-                elseif button == Config.CONTROLS.DOWN then
-                    audio_manager:play_sound("Cursor")
-                    -- TODO: Skip empty character slots
-                    self.character_position = self.character_position + 1
-                    if self.character_position > self.character_position_total then
-                        self.character_position = 1
-                    end
-                    ui_manager:get_widget("MainMenu.Container.Characters.Cursor"):set_default_animation("Position" .. self.character_position)
-                elseif button == Config.CONTROLS.UP then
-                    audio_manager:play_sound("Cursor")
-                    -- TODO: Skip empty character slots
-                    self.character_position = self.character_position - 1
-                    if self.character_position < 1 then
-                        self.character_position = self.character_position_total
-                    end
-                    ui_manager:get_widget("MainMenu.Container.Characters.Cursor"):set_default_animation("Position" .. self.character_position)
-                elseif button == Config.ACTIONS.CONFIRM then
-                    -- TODO: Check for empty character.
-                    audio_manager:play_sound("Cursor")
-                    if self.select_character_for_menu == 2 then -- Magic menu
-                        print("Open magic menu for char in slot " .. self.character_position)
-                    elseif self.select_character_for_menu == 3 then -- Materia menu
-                        UiContainer.current_character_id = Party[self.character_position]
-                        script:request_end_sync(Script.UI, "MateriaMenu", "show", 0)
-                    elseif self.select_character_for_menu == 4 then -- Equip menu
-                        UiContainer.current_character_id = Party[self.character_position]
-                        script:request_end_sync(Script.UI, "EquipMenu", "show", 0)
-                    elseif self.select_character_for_menu == 5 then -- Status menu
-                        print("Open status menu for char in slot " .. self.character_position)
-                    elseif self.select_character_for_menu == 6 then -- Order menu
-                        print("Open order menu for char in slot " .. self.character_position)
-                    elseif self.select_character_for_menu == 7 then -- Limit menu
-                        print("Open limit menu for char in slot " .. self.character_position)
-                    end
-                end
-            end
-        elseif ui_manager:get_widget("MainMenu"):is_visible() == false and MenuSettings.available == true then
-            if button == Config.ACTIONS.MENU and event == "Press" then
-                audio_manager:play_sound("Back")
-                script:request_end_sync(Script.UI, "MainMenu", "show", 0)
-            end
-        else
-            return 0
+    -- Runs when a submenu closes and gives the control back to the main menu.
+    on_resume = function(self)
+        for slot = 1, 3 do
+            self:populate_character(slot)
         end
-
-        return 0
+        ui_manager:get_widget("MainMenu.Container.TimeGil.Gil"):set_text(UiContainer.format_money(Inventory.money))
     end,
 
     --- Opens the main menu.
     --
     -- Populates and updates displayed data before opening. It also pauses the game.
     show = function(self)
-        UiContainer.current_menu = "main"
-        UiContainer.current_submenu = ""
+        self:activate()
         entity_manager:set_paused(true)
         MenuSettings.pause_available = false
         ui_manager:get_widget("MainMenu"):set_visible(true)
-        local characters  = ui_manager:get_widget("MainMenu.Container.Characters")
-        local menu = ui_manager:get_widget("MainMenu.Container.Menu")
-        local menu_cursor = ui_manager:get_widget("MainMenu.Container.Menu.Cursor")
-        local timegil = ui_manager:get_widget("MainMenu.Container.TimeGil")
-        local location = ui_manager:get_widget("MainMenu.Container.Location")
-
 
-        for c = 1, 3 do
-            if Party[c] ~= nil then
-                UiContainer.populate_character_data("MainMenu.Container.Characters.Character" .. tostring(c), Characters[Party[c]])
-                ui_manager:get_widget("MainMenu.Container.Characters.Character" .. tostring(c) .. ".Portrait"):set_image("characters/" .. tostring(Party[c]) .. ".png")
-                if Characters[Party[c]].back_row == 1 then
-                    ui_manager:get_widget("MainMenu.Container.Characters.Character" .. tostring(c) .. ".Portrait"):set_default_animation("RowBack")
-                else
-                    ui_manager:get_widget("MainMenu.Container.Characters.Character" .. tostring(c) .. ".Portrait"):set_default_animation("RowFront")
-                end
-                ui_manager:get_widget("MainMenu.Container.Characters.Character" .. tostring(c) .. ".LimitLevel"):set_text(tostring(Characters[Party[c]].limit.current))
-            else
-                UiContainer.populate_character_data("MainMenu.Container.Characters.Character" .. tostring(c), nil)
-            end
+        for slot = 1, 3 do
+            self:populate_character(slot)
         end
 
         -- TODO: Set portraits of the correct char
 
-        --ui_manager:get_widget("MainMenu.Container.Menu.PHSText"):set_visible(false)
         ui_manager:get_widget("MainMenu.Container.Menu.SaveText"):set_colour(0.4, 0.4, 0.4)
-
-        -- Set location
         ui_manager:get_widget("MainMenu.Container.Location.Text"):set_text(System["MapChanger"].location_name)
+        ui_manager:get_widget("MainMenu.Container.TimeGil.Gil"):set_text(UiContainer.format_money(Inventory.money))
 
-        -- Set money
-        local money_str = tostring(Inventory.money)
-        while #(money_str) < 8 do
-            money_str = " " .. money_str
-        end
-        ui_manager:get_widget("MainMenu.Container.TimeGil.Gil"):set_text(money_str)
-
-        characters:play_animation_stop("Appear")
-        menu:play_animation_stop("Appear")
-        menu_cursor:set_default_animation("Position" .. self.position)
-        timegil:play_animation_stop("Appear")
+        local location = ui_manager:get_widget("MainMenu.Container.Location")
+        ui_manager:get_widget("MainMenu.Container.Characters"):play_animation_stop("Appear")
+        ui_manager:get_widget("MainMenu.Container.Menu"):play_animation_stop("Appear")
+        ui_manager:get_widget("MainMenu.Container.TimeGil"):play_animation_stop("Appear")
         location:play_animation_stop("Appear")
+        -- The cursor starts at the first entry, or where it was left if the cursor memory is on.
+        if not UiContainer.cursor_memory() then self.cursor:set(1) end
+        self.cursor:refresh()
         location:animation_sync()
-        return 0;
+        return 0
     end,
 
-    --- Hides the item menu and goes back to the game.
+    --- Hides the main menu and goes back to the game.
     --
     -- It also unpauses the game.
     hide = function(self)
-        UiContainer.current_menu = ""
-        UiContainer.current_submenu = ""
-        local characters  = ui_manager:get_widget("MainMenu.Container.Characters")
-        local menu = ui_manager:get_widget("MainMenu.Container.Menu")
-        local timegil = ui_manager:get_widget("MainMenu.Container.TimeGil")
+        self:deactivate("")
         local location = ui_manager:get_widget("MainMenu.Container.Location")
 
-        characters:play_animation_stop("Disappear")
-        menu:play_animation_stop("Disappear")
-        timegil:play_animation_stop("Disappear")
+        ui_manager:get_widget("MainMenu.Container.Characters"):play_animation_stop("Disappear")
+        ui_manager:get_widget("MainMenu.Container.Menu"):play_animation_stop("Disappear")
+        ui_manager:get_widget("MainMenu.Container.TimeGil"):play_animation_stop("Disappear")
         location:play_animation_stop("Disappear")
         location:animation_sync()
 
@@ -208,6 +229,6 @@ UiContainer.MainMenu = {
         MenuSettings.pause_available = true
         entity_manager:set_paused(false)
 
-        return 0;
+        return 0
     end,
 }

+ 1 - 0
data/data/scripts/menu/materia_menu.lua

@@ -714,6 +714,7 @@ UiContainer.MateriaMenu = {
         ui_manager:get_widget("MateriaMenu"):set_visible(false)
         UiContainer.current_menu = "main"
         UiContainer.current_submenu = ""
+        UiContainer.resume_menu("main")
         return 0;
     end,
 }

+ 20 - 33
data/data/scripts/menu/menu_helpers.lua

@@ -23,20 +23,15 @@ end
 -- hidden.
 UiContainer.populate_character_data = function(widget, character)
 
-    space_pad = function(val, length)
-        str = tostring(val)
-        while #(str) < length do
-            str = " " .. str
-        end
-        return str
-    end
-
     if character == nil then
         ui_manager:get_widget(widget):set_visible(false)
         return 0
     end
 
-    -- TODO: When MAX HP and MP values are fixed, use them instead of base.
+    -- Maximum HP and MP include the bonuses of equipment and materia. Before the stats are first
+    -- calculated they are not set, so fall back to the base values.
+    local hp_max = character.stats.hp.max or character.stats.hp.base
+    local mp_max = character.stats.mp.max or character.stats.mp.base
 
     local char_id = character.char_id
     ui_manager:get_widget(widget):set_visible(true)
@@ -44,7 +39,7 @@ UiContainer.populate_character_data = function(widget, character)
     -- Critical?
     if character.stats.hp.current == 0 then
         ui_manager:get_widget(widget .. ".Data.HpCurrent"):set_colour(1.0, 0, 0)
-    elseif character.stats.hp.current <= character.stats.hp.base / 4 then
+    elseif character.stats.hp.current <= hp_max / 4 then
         ui_manager:get_widget(widget .. ".Data.HpCurrent"):set_colour(1.0, 1.0, 0)
     else
         ui_manager:get_widget(widget .. ".Data.HpCurrent"):set_colour(0.9, 0.9, 0.9)
@@ -52,42 +47,34 @@ UiContainer.populate_character_data = function(widget, character)
 
     -- Text data
     ui_manager:get_widget(widget .. ".Data.Name"):set_text(character.name)
-    ui_manager:get_widget(widget .. ".Data.LvNumber"):set_text(space_pad(character.level, 3))
-    ui_manager:get_widget(widget .. ".Data.HpCurrent"):set_text(space_pad(character.stats.hp.current, 5))
-    ui_manager:get_widget(widget .. ".Data.HpMax"):set_text(space_pad(character.stats.hp.base, 5))
-    ui_manager:get_widget(widget .. ".Data.MpCurrent"):set_text(space_pad(character.stats.mp.current, 5))
-    ui_manager:get_widget(widget .. ".Data.MpMax"):set_text(space_pad(character.stats.mp.base, 5))
+    ui_manager:get_widget(widget .. ".Data.LvNumber"):set_text(UiContainer.pad_value(character.level, 3))
+    ui_manager:get_widget(widget .. ".Data.HpCurrent"):set_text(UiContainer.pad_value(character.stats.hp.current, 5))
+    ui_manager:get_widget(widget .. ".Data.HpMax"):set_text(UiContainer.pad_value(hp_max, 5))
+    ui_manager:get_widget(widget .. ".Data.MpCurrent"):set_text(UiContainer.pad_value(character.stats.mp.current, 5))
+    ui_manager:get_widget(widget .. ".Data.MpMax"):set_text(UiContainer.pad_value(mp_max, 5))
 
     -- Calculate the HP and MP bar.
     local max_hp_width= ui_manager:get_widget(widget .. ".Data.HpLine"):get_width()
-    local hp_width = math.floor(character.stats.hp.current * max_hp_width / character.stats.hp.base)
+    local hp_width = math.floor(character.stats.hp.current * max_hp_width / hp_max)
     ui_manager:get_widget(widget .. ".Data.HpLineCurrent"):set_width(hp_width)
     local max_mp_width= ui_manager:get_widget(widget .. ".Data.MpLine"):get_width()
-    local mp_width = character.stats.mp.current * max_mp_width / character.stats.mp.base
+    local mp_width = character.stats.mp.current * max_mp_width / mp_max
     ui_manager:get_widget(widget .. ".Data.MpLineCurrent"):set_width(mp_width)
 
     -- Status (Fury/Sadness)
-    local fury = false
-    local sadness = false
+    local status_text = ""
     for _, st in ipairs(character.status) do
-        if st == 4 then
-            sadness = true
+        if st == UiContainer.Status.SADNESS then
+            status_text = "Sadness"
             break
-        elseif st == 5 then
-            fury = true
+        elseif st == UiContainer.Status.FURY then
+            status_text = "Fury"
             break
         end
     end
-    if fury == true then
-        ui_manager:get_widget(widget .. ".Data.Status"):set_colour(0.8, 0, 1)
-        ui_manager:get_widget(widget .. ".Data.Status"):set_text("Fury")
-    elseif sadness == true then
-        ui_manager:get_widget(widget .. ".Data.Status"):set_colour(0.8, 0, 1)
-        ui_manager:get_widget(widget .. ".Data.Status"):set_text("Sadness")
-    elseif sadness == false and fury == false then
-        ui_manager:get_widget(widget .. ".Data.Status"):set_colour(0.8, 0, 1)
-        ui_manager:get_widget(widget .. ".Data.Status"):set_text("")
-    end
+    local status_widget = ui_manager:get_widget(widget .. ".Data.Status")
+    status_widget:set_colour(0.8, 0, 1)
+    status_widget:set_text(status_text)
 
     return 0
 end

+ 467 - 0
data/data/scripts/menu/menu_lib.lua

@@ -0,0 +1,467 @@
+--- Shared building blocks for the menu scripts.
+--
+-- Provides:
+--  * UiContainer.Cursor: a cursor over a fixed list of positions.
+--  * UiContainer.ListCursor: a cursor over a scrollable list.
+--  * UiContainer.Menu: a small state machine for menus (replaces the nested
+--    "if current_menu == ... if current_submenu == ..." ladders).
+--  * Formatting helpers and constants.
+--
+-- Menus still publish UiContainer.current_menu / UiContainer.current_submenu, so menus that haven't
+-- been migrated yet keep working next to the migrated ones.
+
+if UiContainer == nil then UiContainer = {} end
+
+---------------------------------------------------------------------------------------------------
+-- Constants and formatting
+---------------------------------------------------------------------------------------------------
+
+--- Status effect ids shown in menus.
+UiContainer.Status = {
+    SADNESS = 4,
+    FURY = 5,
+}
+
+--- Formats a money amount, right aligned.
+-- @param value The amount of gil.
+-- @param length Width of the field. Defaults to 8.
+UiContainer.format_money = function(value, length)
+    return UiContainer.pad_value(value, length or 8)
+end
+
+--- Formats a number of seconds as HHH:MM.
+UiContainer.format_time = function(seconds)
+    return UiContainer.pad_value(math.floor(seconds / 3600), 3) .. ":"
+        .. UiContainer.zero_pad_value(math.floor((seconds % 3600) / 60), 2)
+end
+
+--- Calculates how much of the way to the next level a character has done.
+--
+-- Uses the EXP curve of the character (see Game.Growth.curves): the EXP needed to attain a level
+-- L is Mod * (L - 1)^2 / 10, where Mod is the gradient of the curve segment for L. The character
+-- stores the EXP still missing for the next level (exp_to_next).
+--
+-- @param character Entry of the Characters table.
+-- @return A number from 0 to 1. 0 if it can't be calculated (e.g. level 99).
+UiContainer.exp_progress = function(character)
+    local definition = Game.Characters[character.char_id]
+    if definition == nil or definition.curves == nil or definition.curves.exp == nil then
+        return 0
+    end
+    local curve = Game.Growth.curves[definition.curves.exp]
+    local level = character.level
+    local target = level + 1
+    if curve == nil or target > 99 then return 0 end
+    local segment
+    if target <= 11 then segment = 0
+    elseif target <= 21 then segment = 1
+    elseif target <= 31 then segment = 2
+    elseif target <= 41 then segment = 3
+    elseif target <= 51 then segment = 4
+    elseif target <= 61 then segment = 5
+    elseif target <= 81 then segment = 6
+    else segment = 7 end
+    local span = math.floor(curve[segment].gradient * level * level / 10)
+    if span <= 0 or character.exp_to_next == nil then return 0 end
+    return math.max(0, math.min(1, 1 - character.exp_to_next / span))
+end
+
+--- Fills a two-line bar (widgets "<prefix>1" and "<prefix>2").
+--
+-- @param prefix Path of the bar widgets, without the final 1/2.
+-- @param fraction Fill amount, 0 to 1.
+-- @param width Width in pixels of the full bar.
+UiContainer.set_bar = function(prefix, fraction, width)
+    fraction = math.max(0, math.min(1, fraction))
+    local pixels = math.floor(fraction * width)
+    for line = 1, 2 do
+        local widget = ui_manager:get_widget(prefix .. tostring(line))
+        widget:set_visible(pixels > 0)
+        widget:set_width(pixels)
+    end
+end
+
+--- Is the cursor memory on? (Cursor: Memory in the config menu.)
+--
+-- If it is, the cursors of the menus and of the battle commands start where they were left. If not
+-- (Initial), they start at the first position.
+UiContainer.cursor_memory = function()
+    return MenuSettings ~= nil and MenuSettings.config ~= nil and MenuSettings.config.cursor == 2
+end
+
+--- Maximum value of the limit bar of a character.
+UiContainer.LIMIT_BAR_MAX = 255
+
+--- Fills the limit bar of a character (see set_bar).
+--
+-- @param prefix Path of the bar widgets, without the final 1/2.
+-- @param character Entry of the Characters table.
+-- @param width Width in pixels of the full bar.
+UiContainer.set_limit_bar = function(prefix, character, width)
+    UiContainer.set_bar(prefix, character.limit.bar / UiContainer.LIMIT_BAR_MAX, width)
+end
+
+--- Plays a menu sound.
+-- @param name One of "Cursor", "Back", "Error", "Window"...
+UiContainer.play_sound = function(name)
+    audio_manager:play_sound(name)
+end
+
+--- Resolves a widget given its full path, or returns the argument if it is already a widget.
+local function resolve_widget(widget)
+    if type(widget) == "string" then
+        return ui_manager:get_widget(widget)
+    end
+    return widget
+end
+
+---------------------------------------------------------------------------------------------------
+-- Cursor
+---------------------------------------------------------------------------------------------------
+
+--- A cursor over a fixed list of positions (1..total).
+--
+-- The cursor widget must define animations "Position1".."PositionN".
+--
+--     self.cursor = UiContainer.Cursor.new{
+--         widget = "ItemMenu.Container.ItemOrder.Cursor",
+--         total = 8,
+--         on_move = function(position) ... end,  -- optional
+--     }
+--
+-- Options:
+--  * widget: Path of the cursor widget (resolved lazily), or a widget. Optional.
+--  * total: Number of positions.
+--  * wrap: Wrap around the ends. Defaults to true.
+--  * sound: Sound played on move. Defaults to "Cursor". false for none.
+--  * on_move: Called with the new position after every move.
+UiContainer.Cursor = {}
+UiContainer.Cursor.__index = UiContainer.Cursor
+
+UiContainer.Cursor.new = function(options)
+    local cursor = setmetatable({}, UiContainer.Cursor)
+    cursor.widget = options.widget
+    cursor.total = options.total or 1
+    cursor.wrap = options.wrap ~= false
+    cursor.sound = options.sound
+    if cursor.sound == nil then cursor.sound = "Cursor" end
+    cursor.on_move = options.on_move
+    cursor.position = options.position or 1
+    return cursor
+end
+
+--- Moves the cursor to a position and updates the widget. Plays no sound.
+function UiContainer.Cursor:set(position)
+    self.position = position
+    if self.widget ~= nil then
+        resolve_widget(self.widget):set_default_animation("Position" .. self.position)
+    end
+    if self.on_move ~= nil then self.on_move(self.position) end
+end
+
+--- Refreshes the widget with the current position.
+function UiContainer.Cursor:refresh()
+    self:set(self.position)
+end
+
+--- Moves by a delta. Returns true if the cursor actually moved.
+function UiContainer.Cursor:move(delta)
+    local target = self.position + delta
+    if target > self.total then
+        if not self.wrap then
+            if self.sound then UiContainer.play_sound("Error") end
+            return false
+        end
+        target = 1
+    elseif target < 1 then
+        if not self.wrap then
+            if self.sound then UiContainer.play_sound("Error") end
+            return false
+        end
+        target = self.total
+    end
+    if self.sound then UiContainer.play_sound(self.sound) end
+    self:set(target)
+    return true
+end
+
+function UiContainer.Cursor:next() return self:move(1) end
+function UiContainer.Cursor:prev() return self:move(-1) end
+
+--- Currently highlighted position.
+function UiContainer.Cursor:index() return self.position end
+
+--- Handles a directional button.
+--
+-- @param button Button name.
+-- @param axis "vertical" (Up/Down) or "horizontal" (Left/Right).
+-- @return true if the button was a direction of the axis (even if the cursor didn't move).
+function UiContainer.Cursor:handle(button, axis)
+    local forward, backward
+    if axis == "horizontal" then
+        forward, backward = Config.CONTROLS.RIGHT, Config.CONTROLS.LEFT
+    else
+        forward, backward = Config.CONTROLS.DOWN, Config.CONTROLS.UP
+    end
+    if button == forward then
+        self:next()
+        return true
+    elseif button == backward then
+        self:prev()
+        return true
+    end
+    return false
+end
+
+--- Handlers for the direction buttons that move a cursor over a grid.
+--
+-- The cursor goes through the cells row by row, and wraps around at the ends of rows and columns.
+-- Up and down keep the column, or go to the last cell of a shorter row. Use them as the `buttons` of
+-- a state of a menu.
+--
+--     buttons = UiContainer.grid_buttons("level_cursor", 2),
+--
+-- @param cursor_name Name of the field of the menu that holds the cursor (a Cursor).
+-- @param columns Number of columns of the grid.
+UiContainer.grid_buttons = function(cursor_name, columns)
+    local buttons = {}
+    for _, button in ipairs({
+        Config.CONTROLS.UP, Config.CONTROLS.DOWN, Config.CONTROLS.LEFT, Config.CONTROLS.RIGHT
+    }) do
+        buttons[button] = function(self, event)
+            local cursor = self[cursor_name]
+            local index = cursor:index() - 1
+            local column = index % columns
+            local row = math.floor(index / columns)
+            local rows = math.ceil(cursor.total / columns)
+            local function row_length(r) return math.min(columns, cursor.total - r * columns) end
+            if button == Config.CONTROLS.LEFT then
+                column = (column - 1) % row_length(row)
+            elseif button == Config.CONTROLS.RIGHT then
+                column = (column + 1) % row_length(row)
+            else
+                row = (row + ((button == Config.CONTROLS.DOWN) and 1 or -1)) % rows
+                column = math.min(column, row_length(row) - 1)
+            end
+            UiContainer.play_sound("Cursor")
+            cursor:set(row * columns + column + 1)
+        end
+    end
+    return buttons
+end
+
+---------------------------------------------------------------------------------------------------
+-- ListCursor
+---------------------------------------------------------------------------------------------------
+
+--- A cursor over a scrollable list: `rows` visible rows over `count` items.
+--
+-- The cursor moves inside the window until it hits an edge, then the window scrolls. It never
+-- wraps.
+--
+-- Options:
+--  * widget: Cursor widget, with animations "Position1".."PositionRows".
+--  * rows: Visible rows.
+--  * count: Total items in the list. Can be changed later through the `count` field.
+--  * on_scroll: Called when the window scrolls (to repopulate the rows).
+--  * on_move: Called after every successful move.
+UiContainer.ListCursor = setmetatable({}, { __index = UiContainer.Cursor })
+UiContainer.ListCursor.__index = UiContainer.ListCursor
+
+UiContainer.ListCursor.new = function(options)
+    local cursor = setmetatable({}, UiContainer.ListCursor)
+    cursor.widget = options.widget
+    cursor.rows = options.rows
+    cursor.total = options.rows
+    cursor.count = options.count or options.rows
+    cursor.sound = options.sound
+    if cursor.sound == nil then cursor.sound = "Cursor" end
+    cursor.on_scroll = options.on_scroll
+    cursor.on_move = options.on_move
+    cursor.position = 1 -- Row inside the window.
+    cursor.first = 1    -- First item shown in the window.
+    return cursor
+end
+
+--- Index of the highlighted item in the whole list.
+function UiContainer.ListCursor:index()
+    return self.first + self.position - 1
+end
+
+--- Resets to the top of the list.
+function UiContainer.ListCursor:reset()
+    self.first = 1
+    self:set(1)
+end
+
+function UiContainer.ListCursor:move(delta)
+    local index = self:index() + delta
+    if index < 1 or index > self.count then
+        if self.sound then UiContainer.play_sound("Error") end
+        return false
+    end
+    if self.sound then UiContainer.play_sound(self.sound) end
+    local row = self.position + delta
+    if row < 1 or row > self.rows then
+        -- Scroll instead of moving the cursor.
+        self.first = self.first + delta
+        if self.on_scroll ~= nil then self.on_scroll(self.first) end
+        if self.on_move ~= nil then self.on_move(self.position) end
+    else
+        self:set(row)
+    end
+    return true
+end
+
+---------------------------------------------------------------------------------------------------
+-- Menu
+---------------------------------------------------------------------------------------------------
+
+--- Menu state machine.
+--
+-- A menu is declared as a table of states. Each state describes how it reacts to the buttons, and
+-- UiContainer.Menu.new() adds the on_button / set_state plumbing:
+--
+--     UiContainer.PauseMenu = UiContainer.Menu.new{
+--         id = "pause",              -- Value of UiContainer.current_menu while open.
+--         initial_state = "",        -- State entered by open(). Defaults to "".
+--         states = {
+--             [""] = {
+--                 cursor = function(self) return self.cursor end,
+--                 axis = "vertical",          -- Directional axis handled by the cursor.
+--                 confirm = function(self, cursor) ... end,
+--                 cancel = function(self) ... end,
+--                 buttons = {                 -- Any other button.
+--                     [Config.ACTIONS.L1] = function(self, event) ... end,
+--                 },
+--             },
+--         },
+--         show = ..., hide = ...,
+--     }
+--
+-- Rules:
+--  * When a menu gives the control back (deactivate), the receiving menu's optional `on_resume`
+--    hook runs, so it can refresh data the submenu may have changed.
+--  * confirm and cancel only fire on "Press" (not on key repeat).
+--  * Directions fire on "Press" and "Repeat".
+--  * cancel is followed by the "Back" sound. confirm is followed by "Cursor". If either handler
+--    returns false, "Error" is played instead.
+--  * A state may set `confirm_sound = false` (or `cancel_sound = false`) when its handler plays its
+--    own sound.
+--  * A state may set `blocked = true` to swallow all input.
+--  * A state may define `capture = function(self, key)`, called with the name of the key when the
+--    player presses the key requested with input_manager:capture_next_key().
+UiContainer.Menu = {}
+
+--- Menus created with UiContainer.Menu.new(), by id.
+UiContainer.menus = {}
+
+--- Tells a menu that it has the control again after a submenu closed.
+--
+-- The menu runs its `on_resume` hook (if it has one), which is the place to refresh whatever the
+-- submenu could have changed (stats, equipment, limit level...). Menus not migrated to
+-- UiContainer.Menu should call this after giving the control back.
+--
+-- @param id Id of the menu receiving the control, e.g. "main".
+UiContainer.resume_menu = function(id)
+    local menu = UiContainer.menus[id]
+    if menu ~= nil and menu.on_resume ~= nil then
+        menu:on_resume()
+    end
+end
+
+local menu_methods = {}
+
+--- Switches to another state of the menu.
+function menu_methods.set_state(self, state)
+    UiContainer.current_submenu = state
+end
+
+--- Gets the current state name.
+function menu_methods.get_state(self)
+    return UiContainer.current_submenu
+end
+
+--- Marks this menu as the active one and enters its initial state.
+function menu_methods.activate(self)
+    UiContainer.current_menu = self.id
+    UiContainer.current_submenu = self.initial_state or ""
+end
+
+--- Gives the control back to another menu.
+--
+-- @param id Id of the menu receiving the control (e.g. "main"). Defaults to `self.parent`.
+function menu_methods.deactivate(self, id)
+    UiContainer.current_menu = id or self.parent or ""
+    UiContainer.current_submenu = ""
+    UiContainer.resume_menu(UiContainer.current_menu)
+end
+
+--- Is this menu the one currently handling the input?
+function menu_methods.is_active(self)
+    return UiContainer.current_menu == self.id
+end
+
+--- Dispatches a button to the current state.
+function menu_methods.on_button(self, button, event, key)
+    if UiContainer.current_menu ~= self.id then
+        if self.on_inactive_button ~= nil then
+            return self:on_inactive_button(button, event) or 0
+        end
+        return 0
+    end
+    local state = self.states[UiContainer.current_submenu]
+    if state == nil or state.blocked then return 0 end
+
+    -- A key asked for with input_manager:capture_next_key() was pressed. The name of the key is the
+    -- third argument.
+    if button == "KEY_CAPTURE" then
+        if state.capture ~= nil then state.capture(self, key) end
+        return 0
+    end
+
+    local pressed = (event == "Press")
+    local cursor = nil
+    if state.cursor ~= nil then cursor = state.cursor(self) end
+
+    if button == Config.ACTIONS.CANCEL then
+        if pressed and state.cancel ~= nil then
+            local result = state.cancel(self)
+            if result == false then
+                UiContainer.play_sound("Error")
+            elseif state.cancel_sound ~= false then
+                UiContainer.play_sound("Back")
+            end
+        end
+    elseif button == Config.ACTIONS.CONFIRM then
+        if pressed and state.confirm ~= nil then
+            local result = state.confirm(self, cursor)
+            if result == false then
+                UiContainer.play_sound("Error")
+            elseif state.confirm_sound ~= false then
+                UiContainer.play_sound("Cursor")
+            end
+        end
+    elseif cursor ~= nil and cursor:handle(button, state.axis) then
+        return 0
+    elseif state.buttons ~= nil and state.buttons[button] ~= nil then
+        state.buttons[button](self, event)
+    end
+    return 0
+end
+
+--- Creates a menu from its definition.
+--
+-- Methods not defined by the menu are filled in with the defaults above (set_state, activate,
+-- deactivate, is_active, on_button).
+UiContainer.Menu.new = function(definition)
+    for name, method in pairs(menu_methods) do
+        if definition[name] == nil then definition[name] = method end
+    end
+    if definition.on_start == nil then
+        definition.on_start = function(self) return 0 end
+    end
+    if definition.states == nil then definition.states = {} end
+    if definition.id ~= nil then UiContainer.menus[definition.id] = definition end
+    return definition
+end

+ 47 - 18
data/data/scripts/menu/name_menu.lua

@@ -1,6 +1,12 @@
 if UiContainer == nil then UiContainer = {} end
 
 --- The name menu.
+--- Is a key one that types a character in the name menu? The letters (their name is one character)
+-- and the space bar.
+local function is_typing_key(key)
+    return key ~= nil and (key == "Space" or #key == 1)
+end
+
 UiContainer.NameMenu = {
 
     --- Indicates if a chocobo is being renamed instead of a character.
@@ -12,6 +18,12 @@ UiContainer.NameMenu = {
     --- Default name.
     default_name = "",
 
+    --- Is the name menu shown when the game asks to name a character?
+    --
+    -- TODO: Turn it on when the menu works with the controller settings (see is_typing_key). While
+    -- it is off, the characters keep their default names and the game goes on.
+    enabled = false,
+
     --- Currently input-ed name.
     name = "",
 
@@ -45,23 +57,26 @@ UiContainer.NameMenu = {
 
     --- Handles button events.
     --
-    -- For the current submenu, handles directional keys, enter and escape events.
-    -- @param button Pressed button string key. "Up", "Left", "Enter" and "Escape" are handled.
+    -- For the current submenu, handles the directions, OK, and the keys that type letters.
+    -- @param button Game event (the directions, OK...) or, for the letters and the space bar,
+    -- the name of the key.
     -- @param event Trigger event. Normally, "Press".
+    -- @param source_key Name of the key that produced the event.
     on_button = function(self, button, event, source_key)
         if UiContainer.current_menu == "name" then
-            if button == Config.ACTIONS.CONFIRM and source_key == "Space" and UiContainer.current_submenu == "" then
+            -- A letter or the space bar that is also the OK key types, it doesn't confirm.
+            if button == Config.ACTIONS.CONFIRM and is_typing_key(source_key) and UiContainer.current_submenu == "" then
                 return 0
             end
             if UiContainer.current_submenu == "confirm" then
-                if button == "Right" then
+                if button == Config.CONTROLS.RIGHT then
                     audio_manager:play_sound("Cursor")
                     self.confirm_position = self.confirm_position + 1
                     if self.confirm_position > self.confirm_position_total then
                         self.confirm_position = 1
                     end
                     ui_manager:get_widget("NameMenu.Container.Confirm.Cursor"):set_default_animation("Position" .. self.confirm_position)
-                elseif button == "Left" then
+                elseif button == Config.CONTROLS.LEFT then
                     audio_manager:play_sound("Cursor")
                     self.confirm_position = self.confirm_position - 1
                     if self.confirm_position < 1 then
@@ -83,26 +98,26 @@ UiContainer.NameMenu = {
                 local character_index
                 if button == "Space" then
                     character_index = string.find(self.letters, " ")
-                else
+                elseif is_typing_key(button) then
                     character_index = string.find(self.letters, button)
                 end
 
                 -- Handle buttons.
-                if button == "Down" then
+                if button == Config.CONTROLS.DOWN then
                     audio_manager:play_sound("Cursor")
                     self.options_position = self.options_position + 1
                     if self.options_position > self.options_position_total then
                         self.options_position = 1
                     end
                     ui_manager:get_widget("NameMenu.Container.Options.Cursor"):set_default_animation("Position" .. self.options_position)
-                elseif button == "Up" then
+                elseif button == Config.CONTROLS.UP then
                     audio_manager:play_sound("Cursor")
                     self.options_position = self.options_position - 1
                     if self.options_position < 1 then
                         self.options_position = self.options_position_total
                     end
                     ui_manager:get_widget("NameMenu.Container.Options.Cursor"):set_default_animation("Position" .. self.options_position)
-                elseif button == "Left" then
+                elseif button == Config.CONTROLS.LEFT then
                     if self.text_cursor > 1 then
                         self.text_cursor = self.text_cursor - 1
                         self.draw_cursor(self)
@@ -110,7 +125,7 @@ UiContainer.NameMenu = {
                     else
                         audio_manager:play_sound("Error")
                     end
-                elseif button == "Right" then
+                elseif button == Config.CONTROLS.RIGHT then
                     if self.text_cursor < self.max_characters then
                         audio_manager:play_sound("Cursor")
                         self.text_cursor = self.text_cursor + 1
@@ -164,6 +179,26 @@ UiContainer.NameMenu = {
         return 0
     end,
 
+    --- Gets the default name of a character.
+    --
+    -- @param id ID of the character.
+    get_default_name = function(self, id)
+        local name = Characters[id].name
+        -- HACK: Hack for Ex-Soldier default. Can this be extracted from menu.lgp?
+        if string.lower(name) == "ex-soldier" then return "Cloud" end
+        return name
+    end,
+
+    --- Gives a character its default name, without showing the menu.
+    --
+    -- Used when the menu is not enabled.
+    --
+    -- @param id ID of the character.
+    apply_default_name = function(self, id)
+        Characters[id].name = self:get_default_name(id)
+        export_character_names()
+    end,
+
     --- Opens the name menu.
     --
     -- Populates and updates displayed data before opening. It also pauses the game. Shows a fade
@@ -180,14 +215,8 @@ UiContainer.NameMenu = {
         ui_manager:get_widget("NameMenu.Container.Confirm.Cursor"):set_default_animation("Position" .. self.confirm_position)
         ui_manager:get_widget("NameMenu.Container.Options.Cursor"):set_default_animation("Position" .. self.options_position)
         ui_manager:get_widget("NameMenu.Container.Character.Portrait"):set_image("characters/" .. tostring(self.id) .. ".png")
-        if (string.lower(Characters[self.id].name) == "ex-soldier") then
-            -- HACK: Hack for Ex-Soldier default. Can this be extracted from menu.lgp?
-            self.default_name = "Cloud"
-            self.name = "Cloud"
-        else
-            self.default_name = Characters[self.id].name
-            self.name = Characters[self.id].name
-        end
+        self.default_name = self:get_default_name(self.id)
+        self.name = self.default_name
         self.text_cursor = math.min(#(self.name) + 1, self.max_characters)
         self.populate_name(self)
         self.draw_cursor(self)

+ 31 - 44
data/data/scripts/menu/pause_menu.lua

@@ -1,64 +1,51 @@
 if UiContainer == nil then UiContainer = {} end
 
---- The pause menu
-UiContainer.PauseMenu = {
+--- The pause menu.
+--
+-- Unlike the other menus it isn't driven by UiContainer.current_menu: it reacts to the START
+-- button while the game is paused.
+UiContainer.PauseMenu = UiContainer.Menu.new{
 
-    --- Cursor position.
-    position = 1,
+    id = "pause",
 
-    --- Maximum cursor position
-    position_total = 2,
-
-    --- Run on creation, does nothing.
-    on_start = function( self )
-        return 0
-    end,
+    --- Cursor over the pause options. 1: Continue, 2: Exit to the begin menu.
+    cursor = UiContainer.Cursor.new{
+        widget = "PauseMenu.Menu.Cursor",
+        total = 2,
+    },
 
     --- Handles button events.
     --
     -- If the game is paused, it handles the navigation in the pause menu. If the game is not
-    -- paused, it only handles the pause event
-    on_button = function( self, button, event )
-        if System:is_paused() == true then
-            local menu_cursor = ui_manager:get_widget( "PauseMenu.Menu.Cursor" )
-            if button == Config.ACTIONS.START and event == "Press" then
-                System:unpause()
-            elseif button == Config.ACTIONS.CONFIRM and event == "Press" then
-                audio_manager:play_sound("Cursor")
-                script:request_end_sync( Script.UI, "PauseMenu", "hide", 0 )
-                if self.position == 2 then
-                    script:request_end_sync( Script.UI, "BeginMenu", "show", 0 )
-                    map("empty")
-                    MenuSettings.pause_available = false
-                else
-                    MenuSettings.available = true
-                end
-            elseif button == "Down" then
-                audio_manager:play_sound("Cursor")
-                self.position = self.position + 1
-                if self.position > self.position_total then
-                    self.position = 1;
-                end
-                menu_cursor:set_default_animation( "Position" .. self.position )
-            elseif button == "Up" then
-                audio_manager:play_sound("Cursor")
-                self.position = self.position - 1
-                if self.position <= 0 then
-                    self.position = self.position_total;
-                end
-                menu_cursor:set_default_animation( "Position" .. self.position )
-            end
-        elseif System:is_paused() == false then
+    -- paused, it only handles the pause event.
+    on_button = function(self, button, event)
+        if System:is_paused() == false then
             if button == Config.ACTIONS.START and event == "Press" then
                 System:pause()
             end
+            return 0
         end
 
+        if button == Config.ACTIONS.START and event == "Press" then
+            System:unpause()
+        elseif button == Config.ACTIONS.CONFIRM and event == "Press" then
+            UiContainer.play_sound("Cursor")
+            script:request_end_sync(Script.UI, "PauseMenu", "hide", 0)
+            if self.cursor:index() == 2 then
+                script:request_end_sync(Script.UI, "BeginMenu", "show", 0)
+                map("empty")
+                MenuSettings.pause_available = false
+            else
+                MenuSettings.available = true
+            end
+        else
+            self.cursor:handle(button, "vertical")
+        end
         return 0
     end,
 
     --- Shows the pause menu.
-    show = function( self )
+    show = function(self)
         ui_manager:get_widget("PauseMenu"):set_visible(true)
     end,
 

+ 313 - 0
data/data/scripts/menu/phs_menu.lua

@@ -0,0 +1,313 @@
+if UiContainer == nil then UiContainer = {} end
+
+local ROOT = "PhsMenu.Container."
+
+--- Slots of the party (left) and of the reserve (right, a grid of 3x3).
+local PARTY_SLOTS = 3
+local RESERVE_COLUMNS = 3
+local RESERVE_ROWS = 3
+local RESERVE_SLOTS = RESERVE_COLUMNS * RESERVE_ROWS
+
+--- The cursor goes over "cells": 1-3 are the party slots, 4-12 the reserve slots.
+local TOTAL_CELLS = PARTY_SLOTS + RESERVE_SLOTS
+
+--- Highest character id the PHS shows (Cid). Young Cloud and Sephiroth can't be chosen, and the
+-- nine characters fit in the party (3) and the reserve (9).
+local MAX_CHARACTER_ID = 8
+
+local function is_party_cell(cell)
+    return cell <= PARTY_SLOTS
+end
+
+--- Gets the cell the cursor goes to when a direction is pressed.
+--
+-- @param cell Current cell.
+-- @param button A direction button.
+-- @return The new cell, or nil if the cursor can't move that way.
+local function neighbour(cell, button)
+    local UP, DOWN = Config.CONTROLS.UP, Config.CONTROLS.DOWN
+    local LEFT, RIGHT = Config.CONTROLS.LEFT, Config.CONTROLS.RIGHT
+    if is_party_cell(cell) then
+        if button == UP then
+            return (cell - 2) % PARTY_SLOTS + 1
+        elseif button == DOWN then
+            return cell % PARTY_SLOTS + 1
+        elseif button == RIGHT then
+            -- First column of the reserve, in the same row.
+            return PARTY_SLOTS + (cell - 1) * RESERVE_COLUMNS + 1
+        end
+        return nil
+    end
+
+    local index = cell - PARTY_SLOTS - 1
+    local column = index % RESERVE_COLUMNS
+    local row = math.floor(index / RESERVE_COLUMNS)
+    if button == LEFT then
+        if column > 0 then return cell - 1 end
+        return math.min(row + 1, PARTY_SLOTS)
+    elseif button == RIGHT then
+        if column < RESERVE_COLUMNS - 1 then return cell + 1 end
+    elseif button == UP then
+        return PARTY_SLOTS + ((row - 1) % RESERVE_ROWS) * RESERVE_COLUMNS + column + 1
+    elseif button == DOWN then
+        return PARTY_SLOTS + ((row + 1) % RESERVE_ROWS) * RESERVE_COLUMNS + column + 1
+    end
+    return nil
+end
+
+--- Is a character locked in its place? Locked characters can't be moved or replaced.
+local function is_locked(char_id)
+    return char_id ~= nil and Characters[char_id].locked == true
+end
+
+--- Number of characters in the party.
+local function party_count()
+    local count = 0
+    for slot = 1, PARTY_SLOTS do
+        if Party[slot] ~= nil then count = count + 1 end
+    end
+    return count
+end
+
+--- Handlers of the directional buttons.
+local function direction_buttons()
+    local buttons = {}
+    for _, button in ipairs({
+        Config.CONTROLS.UP, Config.CONTROLS.DOWN, Config.CONTROLS.LEFT, Config.CONTROLS.RIGHT
+    }) do
+        buttons[button] = function(self, event)
+            local target = neighbour(self.cursor:index(), button)
+            if target ~= nil then
+                UiContainer.play_sound("Cursor")
+                self.cursor:set(target)
+            end
+        end
+    end
+    return buttons
+end
+
+--- The PHS menu: changes the characters of the party.
+--
+-- The party is on the left and the rest of the available characters on the right. A character is
+-- picked with OK, and then OK on another place swaps both:
+--  * party <-> party: changes the order (or moves the character to an empty slot).
+--  * party <-> reserve: the character leaves the party and the other one (if any) takes its place.
+--  * reserve <-> reserve: not allowed.
+-- Locked characters can't be moved or replaced. The party can only be left once it has three
+-- characters, or all the available ones if there are fewer.
+UiContainer.PhsMenu = UiContainer.Menu.new{
+
+    id = "phs",
+    parent = "main",
+
+    --- Cell the cursor is on (see TOTAL_CELLS).
+    cursor = UiContainer.Cursor.new{
+        widget = ROOT .. "Cursor",
+        total = TOTAL_CELLS,
+        sound = false,
+        on_move = function(cell)
+            UiContainer.PhsMenu:update_info(cell)
+        end,
+    },
+
+    --- Cell of the picked character, or nil.
+    held = nil,
+
+    --- IDs of the available characters that aren't in the party, in the order they are shown.
+    reserve = {},
+
+    states = {
+        [""] = {
+            buttons = direction_buttons(),
+            confirm = function(self)
+                return self:on_confirm(self.cursor:index())
+            end,
+            cancel = function(self)
+                if self.held ~= nil then
+                    self:set_held(nil)
+                    return
+                end
+                if not self:is_party_complete() then return false end
+                script:request_end_sync(Script.UI, "PhsMenu", "hide", 0)
+            end,
+        },
+    },
+
+    --- Gets the character in a cell.
+    -- @return Character ID, or nil if the cell is empty.
+    get_character = function(self, cell)
+        if is_party_cell(cell) then return Party[cell] end
+        return self.reserve[cell - PARTY_SLOTS]
+    end,
+
+    --- Puts a character in a cell, or empties it.
+    set_character = function(self, cell, char_id)
+        if is_party_cell(cell) then
+            Party[cell] = char_id
+        else
+            self.reserve[cell - PARTY_SLOTS] = char_id
+        end
+    end,
+
+    --- Number of characters in the reserve. The reserve can have empty cells between them.
+    reserve_count = function(self)
+        local count = 0
+        for slot = 1, RESERVE_SLOTS do
+            if self.reserve[slot] ~= nil then count = count + 1 end
+        end
+        return count
+    end,
+
+    --- Fills the reserve with the available characters that aren't in the party.
+    --
+    -- Characters are packed from the first cell, in order. From then on they keep their cell
+    -- while the party is being formed (see swap).
+    refresh_reserve = function(self)
+        self.reserve = {}
+        for id = 0, MAX_CHARACTER_ID do
+            local character = Characters[id]
+            if character ~= nil and character.enabled == true then
+                local in_party = false
+                for slot = 1, PARTY_SLOTS do
+                    if Party[slot] == id then in_party = true end
+                end
+                if not in_party then
+                    self.reserve[#self.reserve + 1] = id
+                end
+            end
+        end
+    end,
+
+    --- Has the party all the characters it can have?
+    --
+    -- Three are needed, unless there are fewer characters available.
+    is_party_complete = function(self)
+        local available = party_count() + self:reserve_count()
+        return party_count() >= math.min(PARTY_SLOTS, available)
+    end,
+
+    --- Picks a character, or drops the one that was picked.
+    -- @param cell Cell that was selected. nil to drop.
+    set_held = function(self, cell)
+        self.held = cell
+        local held = ui_manager:get_widget(ROOT .. "Held")
+        if cell == nil then
+            held:set_visible(false)
+        else
+            held:set_default_animation("Position" .. tostring(cell))
+            held:set_visible(true)
+        end
+    end,
+
+    --- OK was pressed on a cell.
+    -- @return false if the action isn't allowed.
+    on_confirm = function(self, cell)
+        local character = self:get_character(cell)
+        if self.held == nil then
+            -- Pick a character.
+            if character == nil or is_locked(character) then return false end
+            self:set_held(cell)
+            return
+        end
+
+        -- Picking the same one again drops it.
+        if self.held == cell then
+            self:set_held(nil)
+            return
+        end
+
+        if not self:swap(self.held, cell) then return false end
+        self:set_held(nil)
+        self:populate()
+    end,
+
+    --- Swaps the characters of two cells.
+    -- @return false if it isn't allowed.
+    swap = function(self, from, to)
+        if not is_party_cell(from) and not is_party_cell(to) then return false end
+        local a = self:get_character(from)
+        local b = self:get_character(to)
+        if is_locked(a) or is_locked(b) then return false end
+
+        -- Both cells exchange their contents, so nobody in the reserve changes place.
+        self:set_character(from, b)
+        self:set_character(to, a)
+        set_party(Party[1], Party[2], Party[3])
+        -- TODO: If the leader (first slot) changed, change the player entity of the field too.
+        return true
+    end,
+
+    --- Moves the characters of the party to the first slots, with no gaps between them.
+    compact_party = function(self)
+        local members = {}
+        for slot = 1, PARTY_SLOTS do
+            if Party[slot] ~= nil then members[#members + 1] = Party[slot] end
+        end
+        set_party(members[1], members[2], members[3])
+    end,
+
+    --- Shows the characters of the party and of the reserve.
+    populate = function(self)
+        self:update_info(self.cursor:index())
+        for slot = 1, PARTY_SLOTS do
+            local path = ROOT .. "Party.Slot" .. tostring(slot)
+            local id = Party[slot]
+            if id ~= nil then
+                UiContainer.populate_character_data(path, Characters[id])
+                ui_manager:get_widget(path .. ".Portrait"):set_image("characters/" .. tostring(id) .. ".png")
+            else
+                UiContainer.populate_character_data(path, nil)
+            end
+        end
+        for slot = 1, RESERVE_SLOTS do
+            local path = ROOT .. "Reserve.Slot" .. tostring(slot)
+            local id = self.reserve[slot]
+            ui_manager:get_widget(path):set_visible(id ~= nil)
+            if id ~= nil then
+                ui_manager:get_widget(path .. ".Portrait"):set_image("characters/" .. tostring(id) .. ".png")
+            end
+        end
+    end,
+
+    --- Shows the details of the character under the cursor in the top right window.
+    --
+    -- Only for the reserve: the characters of the party already show them next to their portrait.
+    update_info = function(self, cell)
+        local path = ROOT .. "Info.Character"
+        local id = nil
+        if not is_party_cell(cell) then id = self:get_character(cell) end
+        if id ~= nil then
+            UiContainer.populate_character_data(path, Characters[id])
+        else
+            UiContainer.populate_character_data(path, nil)
+        end
+    end,
+
+    --- Opens the PHS menu.
+    show = function(self)
+        self:activate()
+        -- Empty slots may hold an invalid ID (e.g. -1). Leave them as nil.
+        for slot = 1, PARTY_SLOTS do
+            if Party[slot] ~= nil and Characters[Party[slot]] == nil then Party[slot] = nil end
+        end
+        set_party(Party[1], Party[2], Party[3])
+        -- Whoever is in the party is available, even if no script enabled them.
+        for slot = 1, PARTY_SLOTS do
+            if Party[slot] ~= nil then Characters[Party[slot]].enabled = true end
+        end
+        self:refresh_reserve()
+        self:set_held(nil)
+        self:populate()
+        self.cursor:set(1)
+        ui_manager:get_widget("PhsMenu"):set_visible(true)
+        return 0
+    end,
+
+    --- Closes the PHS menu and goes back to the main menu.
+    hide = function(self)
+        self:compact_party()
+        ui_manager:get_widget("PhsMenu"):set_visible(false)
+        self:deactivate()
+        return 0
+    end,
+}

+ 49 - 0
data/data/scripts/menu/quit_menu.lua

@@ -0,0 +1,49 @@
+if UiContainer == nil then UiContainer = {} end
+
+--- The quit menu.
+--
+-- Only shows a window warning that the unsaved progress will be lost. Confirming quits the game,
+-- declining (or cancelling) goes back to the main menu.
+UiContainer.QuitMenu = UiContainer.Menu.new{
+
+    id = "quit",
+    parent = "main",
+
+    --- Cursor over Yes and No.
+    cursor = UiContainer.Cursor.new{
+        widget = "QuitMenu.Container.Confirm.Cursor",
+        total = 2,
+    },
+
+    states = {
+        [""] = {
+            cursor = function(self) return self.cursor end,
+            axis = "vertical",
+            confirm = function(self, cursor)
+                if cursor:index() == 1 then
+                    console("quit")
+                else
+                    script:request_end_sync(Script.UI, "QuitMenu", "hide", 0)
+                end
+            end,
+            cancel = function(self)
+                script:request_end_sync(Script.UI, "QuitMenu", "hide", 0)
+            end,
+        },
+    },
+
+    --- Opens the window. The cursor starts on No.
+    show = function(self)
+        self:activate()
+        self.cursor:set(2)
+        ui_manager:get_widget("QuitMenu"):set_visible(true)
+        return 0
+    end,
+
+    --- Closes the window and goes back to the main menu.
+    hide = function(self)
+        ui_manager:get_widget("QuitMenu"):set_visible(false)
+        self:deactivate()
+        return 0
+    end,
+}

+ 43 - 97
data/data/scripts/menu/save_menu.lua

@@ -1,75 +1,38 @@
 if UiContainer == nil then UiContainer = {} end
 
---- The item menu.
-UiContainer.SaveMenu = {
+--- The save/load menu.
+UiContainer.SaveMenu = UiContainer.Menu.new{
+
+    id = "save",
 
     --- Current operation ("save/load")
     operation = "save",
 
-    --- Current cursor position in the top bar (1-3).
-    position = 1,
-
-    --- Max cursor position in the top bar.
-    position_total = 3,
-
-    --- Maximum scroll position.
+    --- Maximum number of save slots.
     max_slots = 15,
 
-    --- First visible save slot.
-    first_visible = 1,
+    --- Cursor over the three visible slots, scrolling over all of them.
+    cursor = UiContainer.ListCursor.new{
+        widget = "SaveMenu.Container.Cursor",
+        rows = 3,
+        count = 15,
+        on_scroll = function() UiContainer.SaveMenu:populate_slots() end,
+    },
 
-    --- Run when the menu is creaded.
-    --
-    -- It does nothing.
-    on_start = function(self)
-        return 0
-    end,
-
-    --- Handles button events.
-    --
-    -- For the current submenu, handles directional keys, enter and escape events.
-    -- @param button Pressed direction or action name.
-    -- @param event Trigger event. Normally, "Press".
-    on_button = function(self, button, event)
-        if UiContainer.current_menu == "save" then
-            if UiContainer.current_submenu == "" then
-                if button == Config.ACTIONS.CANCEL and event == "Press" then
-                    audio_manager:play_sound("Back")
-                    script:request_end_sync(Script.UI, "SaveMenu", "hide", 0)
-                elseif button == Config.ACTIONS.CONFIRM then
-                    self.action(self, self.first_visible - 1 + self.position)
-                elseif button == Config.CONTROLS.DOWN then
-                    if self.position < self.position_total and self.first_visible + self.position < self.max_slots then
-                        -- Move cursor down
-                        self.position = self.position + 1
-                        ui_manager:get_widget("SaveMenu.Container.Cursor"):set_default_animation("Position" .. self.position)
-                        audio_manager:play_sound("Cursor")
-                    elseif self.position == self.position_total and self.first_visible + self.position < self.max_slots then
-                        self.first_visible = self.first_visible + 1
-                        self.populate_slots(self)
-                        audio_manager:play_sound("Cursor")
-                    else
-                        audio_manager:play_sound("Error")
-                    end
-                elseif button == Config.CONTROLS.UP then
-                    if self.position > 1 then
-                        -- Move cursor up
-                        self.position = self.position - 1
-                        ui_manager:get_widget("SaveMenu.Container.Cursor"):set_default_animation("Position" .. self.position)
-                        audio_manager:play_sound("Cursor")
-                    elseif self.position == 1 and self.first_visible > 1 then
-                        self.first_visible = self.first_visible - 1
-                        self.populate_slots(self)
-                        audio_manager:play_sound("Cursor")
-                    else
-                        audio_manager:play_sound("Error")
-                    end
-                end
-            elseif UiContainer.current_submenu == "confirm" then
-            end
-        end
-        return 0
-    end,
+    states = {
+        [""] = {
+            cursor = function(self) return self.cursor end,
+            axis = "vertical",
+            cancel = function(self)
+                script:request_end_sync(Script.UI, "SaveMenu", "hide", 0)
+            end,
+            -- The action plays its own sound.
+            confirm_sound = false,
+            confirm = function(self, cursor)
+                self:action(cursor:index())
+            end,
+        },
+    },
 
     --- Opens the save menu.
     --
@@ -84,39 +47,31 @@ UiContainer.SaveMenu = {
             print("before calling show()")
             return 0
         end
-        UiContainer.current_menu = "save"
-        UiContainer.current_submenu = ""
-        if self.operation == "load" then
-            ui_manager:get_widget("SaveMenu.Container.TopBar.DescSave"):set_visible(false)
-            ui_manager:get_widget("SaveMenu.Container.TopBar.DescLoad"):set_visible(true)
-            ui_manager:get_widget("SaveMenu.Container.Title.TitleSave"):set_visible(false)
-            ui_manager:get_widget("SaveMenu.Container.Title.TitleLoad"):set_visible(true)
-        else
-            ui_manager:get_widget("SaveMenu.Container.TopBar.DescSave"):set_visible(true)
-            ui_manager:get_widget("SaveMenu.Container.TopBar.DescLoad"):set_visible(false)
-            ui_manager:get_widget("SaveMenu.Container.Title.TitleSave"):set_visible(true)
-            ui_manager:get_widget("SaveMenu.Container.Title.TitleLoad"):set_visible(false)
-        end
+        self:activate()
+        local loading = (self.operation == "load")
+        local container = "SaveMenu.Container."
+        ui_manager:get_widget(container .. "TopBar.DescSave"):set_visible(not loading)
+        ui_manager:get_widget(container .. "TopBar.DescLoad"):set_visible(loading)
+        ui_manager:get_widget(container .. "Title.TitleSave"):set_visible(not loading)
+        ui_manager:get_widget(container .. "Title.TitleLoad"):set_visible(loading)
         ui_manager:get_widget("SaveMenu"):set_visible(true)
-        UiContainer.current_menu = "save"
-        self.position = 1
-        self.first_visible = 1
-        ui_manager:get_widget("SaveMenu.Container.Cursor"):set_default_animation("Position" .. self.position)
-        self.populate_slots(self)
+        self.cursor.count = self.max_slots
+        self.cursor:reset()
+        self:populate_slots()
         return 0;
     end,
 
     populate_slots = function(self)
         for s = 1, 3 do
-            local slot = self.first_visible + s - 2 -- 0-index
+            local slot = self.cursor.first + s - 2 -- 0-index
             if savemap_manager:is_slot_empty(slot) == true then
                 ui_manager:get_widget("SaveMenu.Container.Slot" .. tostring(s) .. ".Slot"):set_visible(false)
                 ui_manager:get_widget("SaveMenu.Container.Slot" .. tostring(s) .. ".Empty"):set_visible(true)
             else
                 local slot_id = "SaveMenu.Container.Slot" .. tostring(s) .. ".Slot."
-                ui_manager:get_widget(slot_id .. "TimeMoney.Money"):set_text(UiContainer.pad_value(savemap_manager:get_slot_money(slot), 7))
+                ui_manager:get_widget(slot_id .. "TimeMoney.Money"):set_text(UiContainer.format_money(savemap_manager:get_slot_money(slot), 7))
                 local seconds = savemap_manager:get_slot_game_time(slot)
-                local time = UiContainer.pad_value(math.floor(seconds / 3600), 3) .. ":" .. UiContainer.zero_pad_value(math.floor((seconds % 3600) / 60), 2)
+                local time = UiContainer.format_time(seconds)
                 ui_manager:get_widget(slot_id .. "TimeMoney.Time"):set_text(time)
                 ui_manager:get_widget(slot_id .. "Location.Name"):set_text(savemap_manager:get_slot_location_name(slot))
                 local save_party = {
@@ -145,14 +100,10 @@ UiContainer.SaveMenu = {
             if savemap_manager:is_slot_empty(slot - 1) == false and ControlKey ~= savemap_manager:get_slot_control_key(slot - 1) then
                 -- TODO: Confirmation dialog
                 print("OVERRIDE ANOTHER SAVE")
-                save_game(slot - 1, true)
-                audio_manager:play_sound("Cursor")
-                self.populate_slots(self)
-            else
-                save_game(slot - 1, true)
-                audio_manager:play_sound("Cursor")
-                self.populate_slots(self)
             end
+            save_game(slot - 1, true)
+            audio_manager:play_sound("Cursor")
+            self:populate_slots()
         elseif self.operation == "load" then
             audio_manager:play_sound("Window")
             script:request_end_sync(Script.UI, "SaveMenu", "hide", 0)
@@ -164,12 +115,7 @@ UiContainer.SaveMenu = {
     --- Hides the item menu and goes back to the main menu.
     hide = function(self)
         ui_manager:get_widget("SaveMenu"):set_visible(false)
-        if self.operation == "load" then
-            UiContainer.current_menu = "begin"
-        else
-            UiContainer.current_menu = "main"
-        end
-        UiContainer.current_submenu = ""
+        self:deactivate(self.operation == "load" and "begin" or "main")
         savemap_manager:release()
         return 0;
     end,

+ 215 - 0
data/data/scripts/menu/status_menu.lua

@@ -0,0 +1,215 @@
+if UiContainer == nil then UiContainer = {} end
+
+local ROOT = "StatusMenu.Container."
+
+--- Inside width of the bars of the header.
+local BAR_WIDTH = 54
+
+--- Colour of the names of the elements and statuses that apply, and of those that don't.
+local COLOUR_ACTIVE = { 1.0, 1.0, 1.0 }
+local COLOUR_INACTIVE = { 0.45, 0.45, 0.35 }
+
+--- Widgets of the first page with a number, and where its value comes from.
+local NUMBERS = {
+    { "Page1.Attributes.Str", function(c) return c.stats.str.total end },
+    { "Page1.Attributes.Dex", function(c) return c.stats.dex.total end },
+    { "Page1.Attributes.Vit", function(c) return c.stats.vit.total end },
+    { "Page1.Attributes.Mag", function(c) return c.stats.mag.total end },
+    { "Page1.Attributes.Spr", function(c) return c.stats.spr.total end },
+    { "Page1.Attributes.Lck", function(c) return c.stats.lck.total end },
+    { "Page1.Attributes.Atk", function(c) return c.stats.atk end },
+    { "Page1.Attributes.AtkPct", function(c) return c.stats.acc end },
+    { "Page1.Attributes.Def", function(c) return c.stats.def end },
+    { "Page1.Attributes.DefPct", function(c) return c.stats.eva end },
+    { "Page1.Attributes.MAtk", function(c) return c.stats.matk end },
+    { "Page1.Attributes.MDef", function(c) return c.stats.mdef end },
+    { "Page1.Attributes.MDefPct", function(c) return c.stats.meva end },
+    { "Character.Exp", function(c) return c.exp end, 8 },
+    { "Character.Next", function(c) return c.exp_to_next end, 8 },
+    { "Character.LimitLevel", function(c) return c.limit.current end, 2 },
+}
+
+--- Elements of the second page, in the order of the widgets.
+local ELEMENTS = {
+    { Characters.ELEMENT.FIRE, "Fire" }, { Characters.ELEMENT.ICE, "Ice" },
+    { Characters.ELEMENT.BOLT, "Lightning" }, { Characters.ELEMENT.EARTH, "Earth" },
+    { Characters.ELEMENT.POISON, "Poison" }, { Characters.ELEMENT.GRAVITY, "Gravity" },
+    { Characters.ELEMENT.WATER, "Water" }, { Characters.ELEMENT.WIND, "Wind" },
+    { Characters.ELEMENT.HOLY, "Holy" },
+}
+
+--- Statuses of the third page, in the order of the widgets.
+local STATUSES = {
+    { Characters.STATUS.DEATH, "Death" }, { Characters.STATUS.NEAR_DEATH, "Near-death" },
+    { Characters.STATUS.SLEEP, "Sleep" }, { Characters.STATUS.POISON, "Poison" },
+    { Characters.STATUS.SADNESS, "Sadness" }, { Characters.STATUS.FURY, "Fury" },
+    { Characters.STATUS.CONFUSION, "Confusion" }, { Characters.STATUS.SILENCE, "Silence" },
+    { Characters.STATUS.HASTE, "Haste" }, { Characters.STATUS.SLOW, "Slow" },
+    { Characters.STATUS.STOP, "Stop" }, { Characters.STATUS.FROG, "Frog" },
+    { Characters.STATUS.SMALL, "Small" }, { Characters.STATUS.SLOW_NUMB, "Slow-numb" },
+    { Characters.STATUS.PETRIFY, "Petrify" }, { Characters.STATUS.REGEN, "Regen" },
+    { Characters.STATUS.BARRIER, "Barrier" }, { Characters.STATUS.MBARRIER, "MBarrier" },
+    { Characters.STATUS.REFLECT, "Reflect" }, { Characters.STATUS.SHIELD, "Shield" },
+    { Characters.STATUS.D_SENTENCE, "Death-sentence" }, { Characters.STATUS.MANIPULATE, "Manipulate" },
+    { Characters.STATUS.BERSERK, "Berserk" }, { Characters.STATUS.PEERLESS, "Peerless" },
+    { Characters.STATUS.PARALYZED, "Paralyzed" }, { Characters.STATUS.DARKNESS, "Darkness" },
+}
+
+--- Sets a name, white if it applies, gray otherwise.
+local function set_name(path, name, active)
+    local widget = ui_manager:get_widget(path)
+    local colour = active and COLOUR_ACTIVE or COLOUR_INACTIVE
+    -- The text is set after the colour to apply it.
+    widget:set_colour(colour[1], colour[2], colour[3])
+    widget:set_text(name)
+end
+
+--- The status menu. Purely informative.
+--
+-- It has three pages that share the character data: attributes and equipment, elemental effects,
+-- and status effects. OK goes to the next page, looping, and Cancel closes the menu.
+UiContainer.StatusMenu = UiContainer.Menu.new{
+
+    id = "status",
+    parent = "main",
+
+    --- ID of the character being shown.
+    char_id = -1,
+
+    --- Page being shown (1-3).
+    page = 1,
+
+    --- Total number of pages.
+    page_total = 3,
+
+    states = {
+        [""] = {
+            confirm = function(self)
+                self:show_page(self.page % self.page_total + 1)
+            end,
+            cancel = function(self)
+                script:request_end_sync(Script.UI, "StatusMenu", "hide", 0)
+            end,
+        },
+    },
+
+    --- Shows a page and hides the others.
+    show_page = function(self, page)
+        self.page = page
+        for p = 1, self.page_total do
+            ui_manager:get_widget(ROOT .. "Page" .. tostring(p)):set_visible(p == page)
+        end
+    end,
+
+    --- Shows the data shared by every page.
+    populate_header = function(self, character)
+        UiContainer.populate_character_data(ROOT .. "Character", character)
+        ui_manager:get_widget(ROOT .. "Character.Portrait"):set_image("characters/" .. tostring(self.char_id) .. ".png")
+        UiContainer.set_bar(ROOT .. "Character.NextLevelLinePart", UiContainer.exp_progress(character), BAR_WIDTH)
+        UiContainer.set_limit_bar(ROOT .. "Character.LimitLinePart", character, BAR_WIDTH)
+    end,
+
+    --- Shows the slots and materia of a weapon or armor.
+    --
+    -- @param prefix Prefix of the widgets: "Wpn" or "Arm".
+    -- @param item Definition of the weapon or armor.
+    -- @param materia Materia equipped on it. Slots 0-7.
+    populate_slots = function(self, prefix, item, materia)
+        local path = ROOT .. "Page1.Equipment." .. prefix .. ".Slots."
+        for slot = 1, 8 do
+            ui_manager:get_widget(path .. "Slot" .. tostring(slot)):set_visible(#(item.slots) >= slot)
+            local equipped = materia[slot - 1]
+            local widget = ui_manager:get_widget(path .. "Mat" .. tostring(slot))
+            if equipped ~= nil and Game.Materia[equipped.id] ~= nil then
+                widget:set_image("icons/materia_" .. tostring(Game.Materia[equipped.id].type) .. ".png")
+                widget:set_visible(true)
+            else
+                widget:set_visible(false)
+            end
+            -- At every odd slot, if there is a next slot and it's linked, show the linker.
+            if slot % 2 ~= 0 then
+                local linked = #(item.slots) > slot and item.slots[slot] == 1
+                ui_manager:get_widget(path .. "Link" .. tostring(math.floor(slot / 2) + 1)):set_visible(linked)
+            end
+        end
+    end,
+
+    --- Shows the attributes and the equipment.
+    populate_page_1 = function(self, character)
+        for _, entry in ipairs(NUMBERS) do
+            local value = entry[2](character)
+            ui_manager:get_widget(ROOT .. entry[1]):set_text(UiContainer.pad_value(value or 0, entry[3] or 4))
+        end
+
+        local equipment = ROOT .. "Page1.Equipment."
+        local weapon = Game.Items[character.weapon.id]
+        local armor = Game.Items[character.armor.id]
+        ui_manager:get_widget(equipment .. "Wpn.Lbl"):set_text(weapon.name)
+        ui_manager:get_widget(equipment .. "Arm.Lbl"):set_text(armor.name)
+        if character.accessory ~= nil then
+            ui_manager:get_widget(equipment .. "Acc.Lbl"):set_text(Game.Items[character.accessory].name)
+        else
+            ui_manager:get_widget(equipment .. "Acc.Lbl"):set_text("")
+        end
+        ui_manager:get_widget(equipment .. "Wpn.Icon"):set_image("icons/item_weapon_" .. tostring(self.char_id) .. ".png")
+        self:populate_slots("Wpn", weapon, character.weapon.materia)
+        self:populate_slots("Arm", armor, character.armor.materia)
+    end,
+
+    --- Shows the elements the attacks have, and the elements that are halved, nullified or
+    -- absorbed.
+    populate_page_2 = function(self, effects)
+        local mode = Characters.ELEMENT_MODE
+        local groups = {
+            { "Attack", function(element) return effects.elements.attack[element] == true end },
+            { "Halve", function(element) return effects.elements.defense[element] == mode.HALF end },
+            { "Invalid", function(element) return effects.elements.defense[element] == mode.NULLIFY end },
+            { "Absorb", function(element) return effects.elements.defense[element] == mode.ABSORB end },
+        }
+        for _, group in ipairs(groups) do
+            for index, element in ipairs(ELEMENTS) do
+                set_name(ROOT .. "Page2.Table." .. group[1] .. ".Name" .. tostring(index), element[2], group[2](element[1]))
+            end
+        end
+    end,
+
+    --- Shows the statuses the attacks inflict, and the ones the character is protected from.
+    populate_page_3 = function(self, effects)
+        local groups = {
+            { "Attack", effects.status.attack },
+            { "Defend", effects.status.defense },
+        }
+        for _, group in ipairs(groups) do
+            for index, status in ipairs(STATUSES) do
+                set_name(ROOT .. "Page3.Table." .. group[1] .. ".Name" .. tostring(index), status[2], group[2][status[1]] == true)
+            end
+        end
+    end,
+
+    --- Shows the data of the current character in every page.
+    populate = function(self)
+        local character = Characters[self.char_id]
+        local effects = Characters.get_effects(self.char_id)
+        self:populate_header(character)
+        self:populate_page_1(character)
+        self:populate_page_2(effects)
+        self:populate_page_3(effects)
+    end,
+
+    --- Opens the status menu for UiContainer.current_character_id.
+    show = function(self)
+        self.char_id = UiContainer.current_character_id
+        self:activate()
+        self:populate()
+        self:show_page(1)
+        ui_manager:get_widget("StatusMenu"):set_visible(true)
+        return 0
+    end,
+
+    --- Hides the status menu and goes back to the main menu.
+    hide = function(self)
+        ui_manager:get_widget("StatusMenu"):set_visible(false)
+        self:deactivate()
+        return 0
+    end,
+}

+ 124 - 148
data/data/scripts/save.lua

@@ -14,7 +14,8 @@ end
 -- @param force If false, exisiting game data will not be overwritten if control keys are different.
 save_game = function(slot, force)
     savemap_manager:set_control_key(ControlKey)
-    savemap_manager:set_window_colours(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12) -- TODO Actual data
+    -- Settings of the config menu, and the colours of the windows.
+    UiContainer.ConfigMenu:save_settings()
     savemap_manager:set_money(Inventory.money)
     savemap_manager:set_game_time(123456) -- TODO Actual data
     savemap_manager:set_countdown_time(0) -- TODO Actual data
@@ -35,7 +36,7 @@ save_game = function(slot, force)
             if Materia[i].id == Materia.ENEMY_SKILL_ID then
                 for s = 1, 24 do
                     if Materia[i].skills[s] == true then
-                        savemap_manager:set_e_skill_materia(i - 1, s, true)
+                        savemap_manager:set_e_skill_materia(i - 1, s - 1, true)
                     end
                 end
             end
@@ -46,7 +47,6 @@ save_game = function(slot, force)
     local x, y, z = player:get_position()
 
     savemap_manager:set_location(x, y, z, player:get_move_triangle_id(), player:get_rotation(), System["MapChanger"].current_map_name, System["MapChanger"].location_name) -- TODO Actual data
-    -- TODO Settings, when implemented
     for i = 0, 11 do
         if Characters[i] ~= nil and Characters[i].char_id ~= nil then
             local c = Characters[i]
@@ -63,23 +63,17 @@ save_game = function(slot, force)
                 savemap_manager:set_character_limit_learned(i, l - 1, 0, c.limit.learned[l][1], c.limit.uses[l])
                 savemap_manager:set_character_limit_learned(i, l - 1, 1, c.limit.learned[l][2], c.limit.uses[l])
             end
-            for m = 0, 8 do
-                if c.weapon.materia[m] ~= nil then
-                    savemap_manager:set_character_materia(i, true, m, c.weapon.materia[m].id, c.weapon.materia[m].ap)
-                    if c.weapon.materia[m].id == Materia.ENEMY_SKILL_ID then
-                        for s = 1, 24 do
-                            if c.weapon.materia[m].skills[s] == true then
-                                savemap_manager:set_character_e_skill_materie(i, true, m, s, true)
-                            end
-                        end
-                    end
-                end
-                if c.armor[m] ~= nil then
-                    savemap_manager:set_character_materia(i, false, m, c.armor.materia[m].id, c.weapon.materia[m].ap)
-                    if c.weapon.materia[m].id == Materia.ENEMY_SKILL_ID then
-                        for s = 1, 24 do
-                            if c.armor.materia[m].skills[s] == true then
-                                savemap_manager:set_character_e_skill_materie(i, true, m, s, true)
+            for m = 0, 7 do
+                for _, part in ipairs({"weapon", "armor"}) do
+                    local is_weapon = (part == "weapon")
+                    local materia = c[part].materia[m]
+                    if materia ~= nil then
+                        savemap_manager:set_character_materia(i, is_weapon, m, materia.id, materia.ap)
+                        if materia.id == Materia.ENEMY_SKILL_ID then
+                            for s = 1, 24 do
+                                if materia.skills[s] == true then
+                                    savemap_manager:set_character_e_skill_materia(i, is_weapon, m, s - 1, true)
+                                end
                             end
                         end
                     end
@@ -99,7 +93,97 @@ save_game = function(slot, force)
     savemap_manager:save(slot, force or true)
 end
 
---- Laods a game.
+--- Loads the characters from a saved game. Everything save_game saves of them.
+--
+-- The characters that already exist are updated in place, so the data that isn't saved stays.
+--
+-- @param slot The slot to load (0-15).
+local function load_characters(slot)
+    -- The savemap has room for 11 characters.
+    for c = 0, 10 do
+        if savemap_manager:get_slot_character_char_id(slot, c) ~= -1 then
+            local character = Characters[c] or {}
+            Characters[c] = character
+            character.char_id = savemap_manager:get_slot_character_char_id(slot, c)
+            character.name = savemap_manager:get_slot_character_name(slot, c)
+            character.level = savemap_manager:get_slot_character_level(slot, c)
+            character.enabled = savemap_manager:is_slot_character_enabled(slot, c)
+            character.locked = savemap_manager:is_slot_character_locked(slot, c)
+            character.kills = savemap_manager:get_slot_character_kills(slot, c)
+            character.back_row = savemap_manager:is_slot_character_back_row(slot, c)
+            character.exp = savemap_manager:get_slot_character_exp(slot, c)
+            character.exp_to_next = savemap_manager:get_slot_character_exp_to_next(slot, c)
+
+            -- Stats: strength, vitality, magic, spirit, dexterity and luck, then HP and MP.
+            character.stats = character.stats or {}
+            local stat_names = { "str", "vit", "mag", "spr", "dex", "lck" }
+            for index, name in ipairs(stat_names) do
+                character.stats[name] = {
+                    base = savemap_manager:get_slot_character_stat_base(slot, c, index - 1),
+                    bonus = savemap_manager:get_slot_character_stat_extra(slot, c, index - 1),
+                    total = 0,
+                }
+            end
+            -- For HP and MP the "extra" value is the current one. The maximum is calculated.
+            character.stats.hp = {
+                current = savemap_manager:get_slot_character_stat_extra(slot, c, 6),
+                base = savemap_manager:get_slot_character_stat_base(slot, c, 6),
+                max = 65535,
+            }
+            character.stats.mp = {
+                current = savemap_manager:get_slot_character_stat_extra(slot, c, 7),
+                base = savemap_manager:get_slot_character_stat_base(slot, c, 7),
+                max = 65535,
+            }
+
+            -- Limit breaks. Levels are 0-3 in the savemap, and 1-4 in the game.
+            character.limit = {
+                current = savemap_manager:get_slot_character_limit_level(slot, c),
+                bar = savemap_manager:get_slot_character_limit_bar(slot, c),
+                learned = {},
+                uses = {},
+            }
+            for l = 1, 4 do
+                character.limit.learned[l] = {
+                    [1] = savemap_manager:is_slot_character_limit_learned(slot, c, l - 1, 0),
+                    [2] = savemap_manager:is_slot_character_limit_learned(slot, c, l - 1, 1),
+                }
+                character.limit.uses[l] = savemap_manager:get_slot_character_limit_uses(slot, c, l - 1)
+            end
+
+            -- Equipment and its materia (slots 0-7).
+            local accessory = savemap_manager:get_slot_character_accessory_id(slot, c)
+            character.accessory = (accessory >= 0) and accessory or nil
+            character.weapon = { id = savemap_manager:get_slot_character_weapon_id(slot, c), materia = {} }
+            character.armor = { id = savemap_manager:get_slot_character_armor_id(slot, c), materia = {} }
+            for _, part in ipairs({ "weapon", "armor" }) do
+                local is_weapon = (part == "weapon")
+                for m = 0, 7 do
+                    local id = savemap_manager:get_slot_character_materia_id(slot, c, is_weapon, m)
+                    if id >= 0 then
+                        local materia = { id = id, ap = savemap_manager:get_slot_character_materia_ap(slot, c, is_weapon, m) }
+                        if id == Materia.ENEMY_SKILL_ID then
+                            materia.ap = 0
+                            materia.skills = {}
+                            for s = 1, 24 do
+                                materia.skills[s] = savemap_manager:get_slot_character_materia_e_skill_learned(slot, c, is_weapon, m, s - 1)
+                            end
+                        end
+                        character[part].materia[m] = materia
+                    end
+                end
+            end
+
+            -- TODO: Status
+            character.status = {}
+            Characters.recalculate_stats(c)
+        end
+    end
+end
+
+--- Loads a game.
+--
+-- It loads everything save_game saves, and then goes to the field where the game was saved.
 --
 -- @param slot The slot to load (0-15).
 load_game = function(slot)
@@ -109,7 +193,8 @@ load_game = function(slot)
             Banks[b][a] = savemap_manager:get_slot_data(slot, b, a)
         end
     end
-    -- TODO: Window colours
+    -- Settings of the config menu, and the colours of the windows.
+    UiContainer.ConfigMenu:load_settings(slot)
     Inventory.money = savemap_manager:get_slot_money(slot)
     -- TODO: Game time
     -- TODO: Countdown
@@ -119,25 +204,27 @@ load_game = function(slot)
             Party[p] = nil
         end
     end
-    for i = 0, MAX_INVENTORY_SLOTS do
-        Inventory[i] = {item = 0, quantity = 0}
-        Inventory[i].quantity = savemap_manager:get_slot_item_at_pos_qty(slot, i)
-        if Inventory[i].quantity > 0 then
-            Inventory[i].id = savemap_manager:get_slot_item_at_pos_id(slot, i)
+    -- The inventory and the materia are 1-based in the game, and 0-based in the savemap.
+    for i = 1, MAX_INVENTORY_SLOTS do
+        local quantity = savemap_manager:get_slot_item_at_pos_qty(slot, i - 1)
+        if quantity > 0 then
+            Inventory[i] = {
+                item = savemap_manager:get_slot_item_at_pos_id(slot, i - 1),
+                quantity = quantity,
+            }
         else
             Inventory[i] = nil
         end
     end
-    for i = 0, MAX_MATERIA_SLOTS do
-        Materia[i] = {id = -1, ap = 0}
-        Materia[i].id = savemap_manager:get_slot_materia_at_pos_id(slot, i)
-        if Materia[i].id >= 0 then
-            Materia[i].ap = savemap_manager:get_slot_materia_at_pos_ap(slot, i)
-            if Materia[i].id == Materia.ENEMY_SKILL_ID then
+    for i = 1, MAX_MATERIA_SLOTS do
+        local id = savemap_manager:get_slot_materia_at_pos_id(slot, i - 1)
+        if id >= 0 then
+            Materia[i] = { id = id, ap = savemap_manager:get_slot_materia_at_pos_ap(slot, i - 1) }
+            if id == Materia.ENEMY_SKILL_ID then
                 Materia[i].ap = 0
                 Materia[i].skills = {}
                 for s = 1, 24 do
-                    Materia[i].skills[s] = savemap_manager:is_slot_materia_at_pos_e_skill_learned(slot, i, s - 1)
+                    Materia[i].skills[s] = savemap_manager:is_slot_materia_at_pos_e_skill_learned(slot, i - 1, s - 1)
                 end
             end
         else
@@ -145,122 +232,11 @@ load_game = function(slot)
         end
     end
     -- TODO: Stash (same as materia)
-    for i = 0, MAX_KEY_SLOTS do
-        Inventory.key[i] = savemap_manager:get_slot_key_item(slot, i)
-    end
-    -- Characters
-    for c = 0, 11 do
-        Characters[c].char_id = savemap_manager:get_slot_character_char_id(slot, c)
-        if Characters[c].char_id == -1 then
-            Characters[c] = nil
-        else
-            Characters[c] = {}
-            Characters[c].name = savemap_manager:get_slot_character_name(slot, c)
-            Characters[c].level = savemap_manager:get_slot_character_level(slot, c)
-            Characters[c].enabled = savemap_manager:is_slot_character_enabled(slot, c)
-            Characters[c].locked = savemap_manager:is_slot_character_locked(slot, c)
-            Characters[c].kills = savemap_manager:get_slot_character_kills(slot, c)
-            Characters[c].back_row = savemap_manager:is_slot_character_back_row(slot, c)
-            Characters[c].exp = savemap_manager:get_slot_character_exp(slot, c)
-            Characters[c].exp_to_next = savemap_manager:get_slot_character_exp_to_next(slot, c)
-            Characters[c].stats = {}
-            Characters[c].stats.str = {
-                base = savemap_manager:get_slot_character_stat_base(slot, c, 0),
-                bonus = savemap_manager:get_slot_character_stat_extra(slot, c, 0),
-                total = 0
-            }
-            Characters[c].stats.vit = {
-                base = savemap_manager:get_slot_character_stat_base(slot, c, 1),
-                bonus = savemap_manager:get_slot_character_stat_extra(slot, c, 1),
-                total = 0
-            }
-            Characters[c].stats.mag = {
-                base = savemap_manager:get_slot_character_stat_base(slot, c, 2),
-                bonus = savemap_manager:get_slot_character_stat_extra(slot, c, 2),
-                total = 0
-            }
-            Characters[c].stats.spr = {
-                base = savemap_manager:get_slot_character_stat_base(slot, c, 3),
-                bonus = savemap_manager:get_slot_character_stat_extra(slot, c, 3),
-                total = 0
-            }
-            Characters[c].stats.dex = {
-                base = savemap_manager:get_slot_character_stat_base(slot, c, 4),
-                bonus = savemap_manager:get_slot_character_stat_extra(slot, c, 4),
-                total = 0
-            }
-            Characters[c].stats.lck = {
-                base = savemap_manager:get_slot_character_stat_base(slot, c, 5),
-                bonus = savemap_manager:get_slot_character_stat_extra(slot, c, 5),
-                total = 0
-            }
-            Characters[c].stats.hp = {
-                current = savemap_manager:get_slot_character_stat_extra(slot, c, 6),
-                base = savemap_manager:get_slot_character_stat_base(slot, c, 6),
-                max = 65535
-            }
-            Characters[c].stats.mp = {
-                current = savemap_manager:get_slot_character_stat_extra(slot, c, 7),
-                base = savemap_manager:get_slot_character_stat_base(slot, c, 7),
-                max = 65535
-            }
-            Characters[c].limit = {}
-            Characters[c].limit.current = savemap_manager:get_slot_character_limit_level(slot, c)
-            Characters[c].limit.bar = savemap_manager:get_slot_character_limit_bar(slot, c)
-            Characters[c].limit.learned = {}
-            Characters[c].limit.uses = {}
-            for l = 1, 4 do
-                Characters[c].limit.learned[l] = {
-                    [1] = savemap_manager:is_slot_character_limit_learned(slot, c, l, 0),
-                    [2] = savemap_manager:is_slot_character_limit_learned(slot, c, l, 1)
-                }
-                Characters[c].limit.uses[l] = savemap_manager:get_slot_character_limit_uses(slot, c, l)
-            end
-            Characters[c].accessory = savemap_manager:get_slot_character_accessory_id(slot, c)
-            Characters[c].weapon = {}
-            Characters[c].armor = {}
-            Characters[c].weapon.id = savemap_manager:get_slot_character_weapon_id(slot, c)
-            Characters[c].armor.id = savemap_manager:get_slot_character_armor_id(slot, c)
-            Characters[c].weapon.materia = {}
-            Characters[c].armor.materia = {}
-            for i = 0, 8 do
-                Characters[c].weapon.materia[i] = {id = -1, ap = 0}
-                Characters[c].armor.materia[i] = {id = -1, ap = 0}
-                Characters[c].weapon.materia[i].id = savemap_manager:get_slot_character_materia_id(slot, c, true, i)
-                Characters[c].armor.materia[i].id = savemap_manager:get_slot_character_materia_id(slot, c, false, i)
-                if Characters[c].weapon.materia[i].id >= 0 then
-                    Characters[c].weapon.materia[i].ap = savemap_manager:get_slot_character_materia_ap(slot, i, true, i)
-                    if Characters[c].weapon.materia[i].id == Materia.ENEMY_SKILL_ID then
-                        Characters[c].weapon.materia[i].ap = 0
-                        Characters[c].weapon.materia[i].skills = {}
-                        for s = 1, 24 do
-                            Characters[c].weapon.materia[i].skills[s] = savemap_manager:is_slot_character_materia_e_skill_learned(slot, i, c, true, s - 1)
-                        end
-                    end
-                else
-                    Characters[c].weapon.materia[i] = nil
-                end
-                if Characters[c].armor.materia[i].id >= 0 then
-                    Characters[c].armor.materia[i].ap = savemap_manager:get_slot_character_materia_ap(slot, i, false, i)
-                    if Characters[c].armor.materia[i].id == Materia.ENEMY_SKILL_ID then
-                        Characters[c].armor.materia[i].ap = 0
-                        Characters[c].armor.materia[i].skills = {}
-                        for s = 1, 24 do
-                            Characters[c].armor.materia[i].skills[s] = savemap_manager:is_slot_character_materia_e_skill_learned(slot, i, c, false, s - 1)
-                        end
-                    end
-                else
-                    Characters[c].armor.materia[i] = nil
-                end
-            end
-            -- TODO: Status
-            Characters[c].status = {}
-            Characters.recalculate_stats(c)
-        end
-
+    for i = 1, MAX_KEY_SLOTS do
+        Inventory.key[i] = savemap_manager:get_slot_key_item(slot, i - 1)
     end
+    load_characters(slot)
     export_character_names()
-    -- TODO: Settings
     -- TODO: Location
     -- TODO: Update character stats
     -- TODO: Handle world map when implemented

+ 38 - 37
data/data/scripts/system.lua

@@ -117,9 +117,6 @@ end
 -- @param character2 The second member character ID, can be nil.
 -- @param character3 The third member character ID, can be nil.
 set_party = function(character1, character2, character3)
-    Party[1] = character1
-    Party[2] = character2
-    Party[3] = character3
     Party[1] = character1
     Party[2] = character2
     Party[3] = character3
@@ -145,19 +142,19 @@ add_party_character = function(character)
         print("Tried add to party invalid character \"" .. character .. "\".");
         return
     end
-    if Characters[character].party_lock == true then
+    if Characters[character].locked == true then
         print("Tried add to party locked character \"" .. character .. "\".");
         return
     end
     -- Check if character already in party
-    for x = 1, 3 do
+    for i = 1, 3 do
         if Party[i] == character then
             print("Character \"" .. character .. "\" already in party.");
             return
         end
     end
     -- Try to add in first free slot.
-    for x = 1, 3 do
+    for i = 1, 3 do
         if Party[i] == nil then
             Party[i] = character
             export_party_members()
@@ -174,11 +171,11 @@ remove_party_character = function(character)
         print("Tried remove from party invalid character \"" .. character .. "\".");
         return
     end
-    if Characters[character].party_lock == true then
+    if Characters[character].locked == true then
         print("Tried remove from party locked character \"" .. character .. "\".");
         return
     end
-    for x = 1, 3 do
+    for i = 1, 3 do
         if Party[i] == character then
             Party[i] = nil
             export_party_members()
@@ -197,7 +194,7 @@ set_party_lock = function(character, lock)
         print("Tried set/unset party lock to invalid character \"" .. character .. "\".");
         return
     end
-    Characters[character].party_lock = lock;
+    Characters[character].locked = lock;
 end
 
 --- Checks if a character is locked in the party.
@@ -210,7 +207,7 @@ get_party_lock = function(character)
         print("Tried get party lock from invalid character \"" .. character .. "\".");
         return false
     end
-    return Characters[character].party_lock
+    return Characters[character].locked
 end
 
 --- Enables or disables a character is the PHS.
@@ -224,7 +221,7 @@ set_character_enable = function(character, enable)
         print("Tried enable/disable invalid character \"" .. character .. "\".");
         return
     end
-    Characters[character].enable = enable;
+    Characters[character].enabled = enable;
 end
 
 --- Checks if a character is enabled in the PHS.
@@ -238,7 +235,7 @@ get_character_enable = function(character)
         print("Tried get enabled from invalid character \"" .. character .. "\".");
         return false
     end
-    return Characters[character].enable;
+    return Characters[character].enabled;
 end
 
 --- Adds items to the inventory.
@@ -447,9 +444,10 @@ Characters.recalculate_stats = function(id)
     stats.lck.total = stats.lck.base
     stats.hp.max = stats.hp.base
     stats.mp.max = stats.mp.base
-    -- Weapon bonus. HP and MP are percents of base, the rest absolutes.
+    -- Weapon bonus. HP and MP are percents of base, the rest absolutes. The stats are keyed by name,
+    -- so they are walked with pairs (ipairs only walks the array part and found nothing).
     local weapon = Game.Items[Characters[id].weapon.id]
-    for stat, value in ipairs(weapon.stats) do
+    for stat, value in pairs(weapon.stats or {}) do
         if stat == "str" then
             stats.str.total = stats.str.total + value
         elseif stat == "vit" then
@@ -470,7 +468,7 @@ Characters.recalculate_stats = function(id)
     end
     -- Armor bonus. HP and MP are percents of bsae, the rest absolutes.
     local armor = Game.Items[Characters[id].armor.id]
-    for stat, value in ipairs(armor.stats) do
+    for stat, value in pairs(armor.stats or {}) do
         if stat == "str" then
             stats.str.total = stats.str.total + value
         elseif stat == "vit" then
@@ -492,7 +490,7 @@ Characters.recalculate_stats = function(id)
     -- Accesory bonus
     local accessory = Game.Items[Characters[id].accessory]
     if (accessory ~= nil) then
-        for stat, value in ipairs(armor.stats) do
+        for stat, value in pairs(accessory.stats or {}) do
             if stat == "str" then
                 stats.str.total = stats.str.total + value
             elseif stat == "vit" then
@@ -519,12 +517,12 @@ Characters.recalculate_stats = function(id)
             table.insert(materias, Game.Materia[Characters[id].weapon.materia[m].id])
         end
         if Characters[id].armor.materia[m] ~= nil then
-            table.insert(materias, Game.Materia[Characters[id].weapon.materia[m].id])
+            table.insert(materias, Game.Materia[Characters[id].armor.materia[m].id])
         end
     end
     -- Get materia bonuses. All are percents of base.
     for _, materia in ipairs(materias) do
-        for stat, value in ipairs(materia.stats) do
+        for stat, value in pairs(materia.stats or {}) do
             if stat == "str" then
                 stats.str.total = stats.str.total + (value * stats.str.base / 100)
             elseif stat == "vit" then
@@ -625,6 +623,21 @@ Characters.recalculate_stats = function(id)
     Characters[id].stats = stats
 end
 
+--- Sends the materia that doesn't fit in a piece of equipment back to the materia inventory.
+--
+-- @param char_id ID of the character.
+-- @param part "weapon" or "armor".
+-- @param slots Number of materia slots the equipment will have. Slots are 0-7 in the character.
+Characters.remove_excess_materia = function(char_id, part, slots)
+    local materia = Characters[char_id][part].materia
+    for m = slots, 7 do
+        if materia[m] ~= nil then
+            Materia.add(materia[m].id, materia[m].ap)
+            materia[m] = nil
+        end
+    end
+end
+
 --- Equips an item on a character.
 --
 -- Works for weapons, armors and accessories. If there is a previously equiped item, it
@@ -644,15 +657,15 @@ Characters.equip = function(char_id, item_id)
         print("Tried to equip item on wrong character: " .. tostring(char_id))
         return false
     elseif Game.Items[item_id] == nil then
-        print("Tried to equip invalid item: " .. tostring(character))
+        print("Tried to equip invalid item: " .. tostring(item_id))
         return false
     elseif Inventory.get_item_quantity(item_id) < 1 then
-        print("Tried to equip item not in inventory: " .. item_id .. " (" .. Game.Items[item.id].name .. ")")
+        print("Tried to equip item not in inventory: " .. item_id .. " (" .. Game.Items[item_id].name .. ")")
         return false
     end
     local type = Game.Items[item_id].type
     if type ~= Inventory.ITEM_TYPE.WEAPON and type ~= Inventory.ITEM_TYPE.ARMOR and type ~= Inventory.ITEM_TYPE.ACCESSORY then
-        print("Tried to equip non-equipable item: " .. item_id .. " (" .. Game.Items[item.id].name .. ")")
+        print("Tried to equip non-equipable item: " .. item_id .. " (" .. Game.Items[item_id].name .. ")")
         return false
     end
     local equipable = false
@@ -663,7 +676,7 @@ Characters.equip = function(char_id, item_id)
         end
     end
     if equipable == false then
-        print("Tried to equip item " .. tostring(item_id) .. " (" .. Game.Items[item.id].name .. ") on a non valid character: " .. tostring(char_id) .. " (" .. Characters[char_id].name .. ")")
+        print("Tried to equip item " .. tostring(item_id) .. " (" .. Game.Items[item_id].name .. ") on a non valid character: " .. tostring(char_id) .. " (" .. Characters[char_id].name .. ")")
         return false
     end
 
@@ -677,25 +690,13 @@ Characters.equip = function(char_id, item_id)
         Inventory.remove_item(item_id, 1)
     elseif type == Inventory.ITEM_TYPE.WEAPON then
         -- Weapon. Remove excess materia, send previuos one to the inventory.
-        for m = 0, 7 do
-            if Characters[char_id].weapon.materia[i] ~= nil and #(Game.Items[item_id].slots) < m then
-                -- Materia to inventory
-                Materia.add(Characters[char_id].weapon.materia[i].id, Characters[char_id].weapon.materia[i].ap)
-                Characters[char_id].weapon.materia[i] = nil
-            end
-        end
+        Characters.remove_excess_materia(char_id, "weapon", #(Game.Items[item_id].slots))
         Inventory.add_item(Characters[char_id].weapon.id, 1)
         Characters[char_id].weapon.id = item_id
         Inventory.remove_item(item_id, 1)
     elseif type == Inventory.ITEM_TYPE.ARMOR then
         -- Armor. Remove excess materia, send previuos one to the inventory.
-        for m = 0, 7 do
-            if Characters[char_id].armor.materia[i] ~= nil and #(Game.Items[item_id].slots) < m then
-                -- Materia to inventory
-                Materia.add(Characters[char_id].armor.materia[i].id, Characters[char_id].armor.materia[i].ap)
-                Characters[char_id].armor.materia[i] = nil
-            end
-        end
+        Characters.remove_excess_materia(char_id, "armor", #(Game.Items[item_id].slots))
         Inventory.add_item(Characters[char_id].armor.id, 1)
         Characters[char_id].armor.id = item_id
         Inventory.remove_item(item_id, 1)
@@ -723,7 +724,7 @@ end
 Materia.add = function(id, ap)
     local ap = ap or 0
     if Game.Materia[id] == nil then
-        print("Tried to add invalid materia \"" .. item .. "\".");
+        print("Tried to add invalid materia \"" .. tostring(id) .. "\".");
         return
     end
     -- TODO Cap AP?

+ 114 - 2
data/data/texts/english/menu.xml

@@ -60,11 +60,9 @@
     <text name="ItemMenuOrder">Order</text>
     <text name="ItemMenuKey">Key Items</text>
     <text name="ItemMenuStatus">Status</text>
-    <text name="ItemMenuOrder">Order</text>
     <text name="ItemMenuLimit">Limit</text>
     <text name="ItemMenuConfig">Config</text>
     <text name="ItemMenuPHS">PHS</text>
-    <text name="MainMenuSave">Save</text>
     <text name="ItemMenuOrder1">Custom</text>
     <text name="ItemMenuOrder2">Field</text>
     <text name="ItemMenuOrder3">Battle</text>
@@ -137,4 +135,118 @@
     <text name="BattleUiTime">TIME</text>
     <text name="GameOverText">GAME OVER</text>
     <text name="BattleUiDebugExit">Debug: Press 'K' to end battle.</text>
+
+    <text name="StatusMenuStr"><colour value="0 0.8 1">Strength</colour></text>
+    <text name="StatusMenuDex"><colour value="0 0.8 1">Dexterity</colour></text>
+    <text name="StatusMenuVit"><colour value="0 0.8 1">Vitality</colour></text>
+    <text name="StatusMenuMag"><colour value="0 0.8 1">Magic</colour></text>
+    <text name="StatusMenuSpr"><colour value="0 0.8 1">Spirit</colour></text>
+    <text name="StatusMenuLck"><colour value="0 0.8 1">Luck</colour></text>
+    <text name="StatusMenuAttack"><colour value="0 0.8 1">Attack</colour></text>
+    <text name="StatusMenuAttackPct"><colour value="0 0.8 1">Attack%</colour></text>
+    <text name="StatusMenuDefense"><colour value="0 0.8 1">Defense</colour></text>
+    <text name="StatusMenuDefensePct"><colour value="0 0.8 1">Defense%</colour></text>
+    <text name="StatusMenuMagicAtk"><colour value="0 0.8 1">Magic atk</colour></text>
+    <text name="StatusMenuMagicDef"><colour value="0 0.8 1">Magic def</colour></text>
+    <text name="StatusMenuMagicDefPct"><colour value="0 0.8 1">Magic def%</colour></text>
+    <text name="StatusMenuExp"><colour value="0 0.8 1">EXP:</colour></text>
+    <text name="StatusMenuNextLevel"><colour value="0 0.8 1">next level:</colour></text>
+    <text name="StatusMenuLimitLevel"><colour value="0 0.8 1">Limit level:</colour></text>
+    <text name="StatusMenuElement"><colour value="0 0.8 1">Element</colour></text>
+    <text name="StatusMenuEffect"><colour value="0 0.8 1">Effect</colour></text>
+    <text name="StatusMenuAttackGroup"><colour value="0 0.8 1">Attack</colour></text>
+    <text name="StatusMenuHalve"><colour value="0 0.8 1">Halve</colour></text>
+    <text name="StatusMenuInvalid"><colour value="0 0.8 1">Invalid</colour></text>
+    <text name="StatusMenuAbsorb"><colour value="0 0.8 1">Absorb</colour></text>
+    <text name="StatusMenuDefend"><colour value="0 0.8 1">Defend</colour></text>
+    <text name="LimitMenuTitle">Limit</text>
+    <text name="LimitMenuSet">Set</text>
+    <text name="LimitMenuCheck">Check</text>
+    <text name="LimitMenuLimitLevel">Limit level</text>
+    <text name="LimitMenuConfirm1">To change BREAK LEVEL,</text>
+    <text name="LimitMenuConfirm2">it will begin from Limit Point 0.</text>
+    <text name="LimitMenuConfirm3">Change BREAK LEVEL?</text>
+    <text name="QuitMenuLine1">Any unsaved progress will be lost.</text>
+    <text name="QuitMenuLine2">Do you really want to quit the game?</text>
+    <text name="PhsMenuTitle">PHS</text>
+    <text name="PhsMenuHelp">Please make a party of three.</text>
+    <text name="ConfigMenuTitle">Config</text>
+    <text name="ConfigMenuLabelWindowColor"><colour value="0 0.8 1">Window color</colour></text>
+    <text name="ConfigMenuLabelSound"><colour value="0 0.8 1">Sound</colour></text>
+    <text name="ConfigMenuLabelController"><colour value="0 0.8 1">Controller</colour></text>
+    <text name="ConfigMenuLabelCursor"><colour value="0 0.8 1">Cursor</colour></text>
+    <text name="ConfigMenuLabelAtb"><colour value="0 0.8 1">ATB</colour></text>
+    <text name="ConfigMenuLabelBattleSpeed"><colour value="0 0.8 1">Battle speed</colour></text>
+    <text name="ConfigMenuLabelBattleMessage"><colour value="0 0.8 1">Battle message</colour></text>
+    <text name="ConfigMenuLabelFieldMessage"><colour value="0 0.8 1">Field message</colour></text>
+    <text name="ConfigMenuLabelMovement"><colour value="0 0.8 1">Movement</colour></text>
+    <text name="ConfigMenuLabelCameraAngle"><colour value="0 0.8 1">Camera angle</colour></text>
+    <text name="ConfigMenuLabelMagicOrder"><colour value="0 0.8 1">Magic order</colour></text>
+    <text name="ConfigMenuInitial">Initial</text>
+    <text name="ConfigMenuMemory">Memory</text>
+    <text name="ConfigMenuActive">Active</text>
+    <text name="ConfigMenuRecommended">Recommended</text>
+    <text name="ConfigMenuWait">Wait</text>
+    <text name="ConfigMenuFast">Fast</text>
+    <text name="ConfigMenuSlow">Slow</text>
+    <text name="ConfigMenuAuto">Auto</text>
+    <text name="ConfigMenuFix">Fix</text>
+    <text name="ConfigMenuWalk">Walk</text>
+    <text name="ConfigMenuRun">Run</text>
+    <text name="ConfigMenuMagicNo">No.</text>
+    <text name="ConfigMenuMagicOrder1">restore attack indirect</text>
+    <text name="ConfigMenuMagicOrder2">restore indirect attack</text>
+    <text name="ConfigMenuMagicOrder3">attack restore indirect</text>
+    <text name="ConfigMenuMagicOrder4">attack indirect restore</text>
+    <text name="ConfigMenuMagicOrder5">indirect restore attack</text>
+    <text name="ConfigMenuMagicOrder6">indirect attack restore</text>
+    <text name="ConfigMenuHelp1">Select colors for each corner of the window</text>
+    <text name="ConfigMenuHelp2">Choose the sound settings</text>
+    <text name="ConfigMenuHelp3">Choose the controller settings</text>
+    <text name="ConfigMenuHelp4">Choose whether the cursor starts at the top or remembers its last position</text>
+    <text name="ConfigMenuHelp5">Choose how time flows in battle: Active, Recommended or Wait</text>
+    <text name="ConfigMenuHelp6">Set the speed of the battles</text>
+    <text name="ConfigMenuHelp7">Set how fast battle messages are displayed</text>
+    <text name="ConfigMenuHelp8">Set how fast field messages are displayed</text>
+    <text name="ConfigMenuHelp9">Choose whether the character walks or runs</text>
+    <text name="ConfigMenuHelp10">Choose automatic or fixed camera angle in battle</text>
+    <text name="ConfigMenuHelp11">Set the order of the magic menu</text>
+    <text name="ConfigMenuColourRed"><colour value="0 0.8 1">Red</colour></text>
+    <text name="ConfigMenuColourGreen"><colour value="0 0.8 1">Green</colour></text>
+    <text name="ConfigMenuColourBlue"><colour value="0 0.8 1">Blue</colour></text>
+    <text name="ConfigMenuSoundMusic"><colour value="0 0.8 1">Music</colour></text>
+    <text name="ConfigMenuSoundEffects"><colour value="0 0.8 1">Effects</colour></text>
+    <text name="ConfigMenuActiveOff"><colour value="0.45 0.45 0.35">Active</colour></text>
+    <text name="ConfigMenuAutoOff"><colour value="0.45 0.45 0.35">Auto</colour></text>
+    <text name="ConfigMenuFixOff"><colour value="0.45 0.45 0.35">Fix</colour></text>
+    <text name="ConfigMenuInitialOff"><colour value="0.45 0.45 0.35">Initial</colour></text>
+    <text name="ConfigMenuMemoryOff"><colour value="0.45 0.45 0.35">Memory</colour></text>
+    <text name="ConfigMenuRecommendedOff"><colour value="0.45 0.45 0.35">Recommended</colour></text>
+    <text name="ConfigMenuRunOff"><colour value="0.45 0.45 0.35">Run</colour></text>
+    <text name="ConfigMenuWaitOff"><colour value="0.45 0.45 0.35">Wait</colour></text>
+    <text name="ConfigMenuWalkOff"><colour value="0.45 0.45 0.35">Walk</colour></text>
+    <text name="ConfigMenuControlDefault"><colour value="0 0.8 1">Default settings</colour></text>
+    <text name="ConfigMenuControlUp"><colour value="0 0.8 1">Up</colour></text>
+    <text name="ConfigMenuControlDown"><colour value="0 0.8 1">Down</colour></text>
+    <text name="ConfigMenuControlLeft"><colour value="0 0.8 1">Left</colour></text>
+    <text name="ConfigMenuControlRight"><colour value="0 0.8 1">Right</colour></text>
+    <text name="ConfigMenuControlOk"><colour value="0 0.8 1">OK</colour></text>
+    <text name="ConfigMenuControlCancel"><colour value="0 0.8 1">Cancel</colour></text>
+    <text name="ConfigMenuControlMenu"><colour value="0 0.8 1">Menu</colour></text>
+    <text name="ConfigMenuControlExtra"><colour value="0 0.8 1">Extra</colour></text>
+    <text name="ConfigMenuControlSelect"><colour value="0 0.8 1">Select</colour></text>
+    <text name="ConfigMenuControlStart"><colour value="0 0.8 1">Start</colour></text>
+    <text name="ConfigMenuControlL1"><colour value="0 0.8 1">L1</colour></text>
+    <text name="ConfigMenuControlR1"><colour value="0 0.8 1">R1</colour></text>
+    <text name="ConfigMenuControlRun"><colour value="0 0.8 1">Run</colour></text>
+    <text name="ConfigMenuHelp12">Press the new key for this action. Esc cancels</text>
+    <text name="MagicMenuTitle">Magic</text>
+    <text name="MagicMenuMagic">Magic</text>
+    <text name="MagicMenuSummon">Summon</text>
+    <text name="MagicMenuESkill">E.Skill</text>
+    <text name="MagicMenuAddedAbility"><colour value="0 0.8 1">Added-Ability</colour></text>
+    <text name="MagicMenuNothing">Nothing.</text>
+    <text name="MagicMenuMpNeeded"><colour value="0 0.8 1">MP needed</colour></text>
+    <text name="MagicMenuUses"><colour value="0 0.8 1">Uses</colour></text>
+    <text name="StatusMenuTitle">Status</text>
 </texts>

+ 23 - 0
src/core/AudioManager.cpp

@@ -13,6 +13,7 @@
  * GNU General Public License for more details.
  */
 
+#include <algorithm>
 #include <iostream>
 #include <list>
 #include <boost/thread.hpp>
@@ -218,6 +219,21 @@ void AudioManager::ScriptPlaySounds(
     ScriptPlaySound(name1);
 }
 
+void AudioManager::ScriptSetMusicVolume(const float volume){
+    if (!initialized_) return;
+    boost::recursive_mutex::scoped_lock lock(update_mutex_);
+    music_.SetGain(volume);
+    battle_music_.SetGain(volume);
+    // The audio of the movies follows the music volume.
+    movie_.SetGain(volume);
+}
+
+void AudioManager::ScriptSetSoundVolume(const float volume){
+    if (!initialized_) return;
+    boost::recursive_mutex::scoped_lock lock(update_mutex_);
+    fx_.SetGain(volume);
+}
+
 void AudioManager::MusicStop(){
     boost::recursive_mutex::scoped_lock lock(update_mutex_);
     music_.Stop();
@@ -371,6 +387,7 @@ void AudioManager::Player::Play(const Ogre::String &file){
     alGenBuffers(channel_buffer_number_, buffers);
     // Set source parameters
     alSourcei(source_, AL_LOOPING, AL_FALSE);
+    alSourcef(source_, AL_GAIN, gain_);
     // Fill buffers
     for (int i = 0; i < channel_buffer_number_; ++ i){
         ALsizei buffer_size = FillBuffer();
@@ -392,6 +409,12 @@ void AudioManager::Player::Play(const Ogre::String &file){
     alSourcePlay(source_);
 }
 
+void AudioManager::Player::SetGain(const float gain){
+    boost::recursive_mutex::scoped_lock lock(*update_mutex_);
+    gain_ = std::max(0.0f, std::min(1.0f, gain));
+    if (!file_.empty() && alIsSource(source_)) alSourcef(source_, AL_GAIN, gain_);
+}
+
 void AudioManager::Player::Stop(){
     boost::recursive_mutex::scoped_lock lock(*update_mutex_);
     // Nothing to stop if there is no track. The id of a source that was deleted can be given to

+ 35 - 0
src/core/AudioManager.h

@@ -304,6 +304,27 @@ class AudioManager : public Manager, public Ogre::Singleton<AudioManager>{
          */
         void ScriptPlayMusicOnce(const char* name);
 
+        /**
+         * Sets the volume of the music (field, battle and world tracks) and of the audio of the
+         * movies.
+         *
+         * It applies right away to the track that is playing and to the ones that start later. To
+         * be called from Lua scripts.
+         *
+         * @param[in] volume Volume, from 0 (muted) to 1 (maximum).
+         */
+        void ScriptSetMusicVolume(const float volume);
+
+        /**
+         * Sets the volume of the sound effects.
+         *
+         * It applies right away to the sound that is playing and to the ones that start later. To
+         * be called from Lua scripts.
+         *
+         * @param[in] volume Volume, from 0 (muted) to 1 (maximum).
+         */
+        void ScriptSetSoundVolume(const float volume);
+
         /**
          * Stops the battle music and lets the field or world map music continue.
          *
@@ -362,6 +383,15 @@ class AudioManager : public Manager, public Ogre::Singleton<AudioManager>{
                  */
                 void SetLoop(const float loop);
 
+                /**
+                 * Sets the volume of the player.
+                 *
+                 * Applies to the track that is playing, and is kept for the ones that start later.
+                 *
+                 * @param[in] gain Volume, from 0 (muted) to 1 (maximum).
+                 */
+                void SetGain(const float gain);
+
                 /**
                  * Updates the audio player.
                  *
@@ -424,6 +454,11 @@ class AudioManager : public Manager, public Ogre::Singleton<AudioManager>{
                  */
                 ALuint source_;
 
+                /**
+                 * Volume of the player, from 0 to 1.
+                 */
+                float gain_ = 1.0f;
+
                 /**
                  * Fills the audio buffer.
                  *

+ 13 - 8
src/core/EntityManager.cpp

@@ -259,7 +259,8 @@ void EntityManager::UpdateField(){
                             speed = entity_[i]->GetMoveWalkSpeed();
                             // TODO: Set as a constant.
                             // Or even better, as a configurable variable.
-                            if (player_run_ == true) speed *= 4;
+                            // The run key inverts the default movement.
+                            if (player_run_ != player_default_run_) speed *= 4;
                         }
                         else speed = entity_[i]->GetMoveAutoSpeed();
                         float remaining_time = Timer::getSingleton().GetGameTimeDelta();
@@ -778,6 +779,8 @@ void EntityManager::ScriptPlayerLock(const bool lock){
 
 bool EntityManager::IsPlayerLocked() const{return player_lock_;}
 
+void EntityManager::SetPlayerDefaultRun(const bool run){player_default_run_ = run;}
+
 void EntityManager::SetPlayerMoveRotation(const Ogre::Radian rotation){
     if (IsBattleModule()){
         LOG_WARNING("Tried to set player move rotation the EntityManager is in battle mode.");
@@ -920,14 +923,16 @@ bool EntityManager::IsKeyOn(unsigned int key_code){
     // Some scene where the player can't move but must press a button?
     if (player_lock_) return false;
     // The key code of the scripts is a mask of the pad buttons, and the check is true if any of
-    // them is pressed. Translate each to its key. For example 32 is "Circle/Action", the space bar.
-    // TODO: Get values from config.lua
-    static const struct{unsigned int mask; int key;} KEYS[] = {
-      {4, OIS::KC_PGUP}, {8, OIS::KC_PGDOWN}, {32, OIS::KC_SPACE}, {64, OIS::KC_ESCAPE},
-      {4096, OIS::KC_UP}, {8192, OIS::KC_RIGHT}, {16384, OIS::KC_DOWN}, {32768, OIS::KC_LEFT}
+    // them is pressed. Translate each to its game event (see bind_game_event in config.cfg), so
+    // the keys chosen in the controller settings are the ones that count. For example 32 is
+    // "Circle/Action", the OK button.
+    static const struct{unsigned int mask; const char* event;} BUTTONS[] = {
+      {1, "L2"}, {2, "R2"}, {4, "L1"}, {8, "R1"}, {16, "MENU"}, {32, "CONFIRM"}, {64, "CANCEL"},
+      {128, "EXTRA"}, {256, "SELECT"}, {2048, "START"},
+      {4096, "Up"}, {8192, "Right"}, {16384, "Down"}, {32768, "Left"}
     };
-    for (const auto& entry : KEYS)
-        if ((key_code & entry.mask) && InputManager::getSingleton().IsButtonPressed(entry.key))
+    for (const auto& entry : BUTTONS)
+        if ((key_code & entry.mask) && InputManager::getSingleton().IsGameEventPressed(entry.event))
             return true;
     return false;
 }

+ 16 - 0
src/core/EntityManager.h

@@ -377,6 +377,17 @@ class EntityManager : public Manager, public Ogre::Singleton<EntityManager>{
          */
         void ScriptPlayerLock(const bool lock);
 
+        /**
+         * Sets if the player moves running by default.
+         *
+         * If true, the player runs unless the run key is held, which makes it walk. If false (the
+         * default), the player walks unless the run key is held. To be called from Lua scripts
+         * (movement setting of the config menu).
+         *
+         * @param[in] run True to run by default, false to walk by default.
+         */
+        void SetPlayerDefaultRun(const bool run);
+
         /**
          * Checks if the player movement is locked.
          *
@@ -840,6 +851,11 @@ class EntityManager : public Manager, public Ogre::Singleton<EntityManager>{
          */
         bool player_run_;
 
+        /**
+         * Indicates if the player runs by default, and the run key makes it walk instead.
+         */
+        bool player_default_run_ = false;
+
         /**
          * List of triggers.
          */

+ 88 - 0
src/core/InputManager.cpp

@@ -13,6 +13,7 @@
  * GNU General Public License for more details.
  */
 
+#include <algorithm>
 #include "core/Console.h"
 #include "core/InputManager.h"
 #include "core/InputManagerCommands.h"
@@ -51,6 +52,18 @@ void InputManager::Reset(){
 }
 
 void InputManager::ButtonPressed(int button, char text, bool down){
+    // A script asked for the next key (to let the player choose one): report it as an event and do
+    // nothing else with it.
+    if (capture_next_key_ == true && down == true){
+        capture_next_key_ = false;
+        VGears::Event capture_event;
+        capture_event.type = VGears::ET_KEY_PRESS;
+        capture_event.param1 = button;
+        capture_event.param2 = text;
+        capture_event.event = "KEY_CAPTURE";
+        event_queue_.push_back(capture_event);
+        return;
+    }
     if (button_state_[button] != down){
         button_state_[button] = down;
         button_text_[button] = text;
@@ -130,6 +143,22 @@ void InputManager::Update(){
 
 bool InputManager::IsButtonPressed(int button) const{return button_state_[button];}
 
+bool InputManager::IsGameEventPressed(const Ogre::String& event) const{
+    if (Console::getSingleton().IsVisible() == true) return false;
+    for (const BindGameEventInfo& bind : bind_game_events_){
+        if (bind.event != event || bind.buttons.empty()) continue;
+        bool all_pressed = true;
+        for (const int button : bind.buttons){
+            if (IsButtonPressed(button) == false){
+                all_pressed = false;
+                break;
+            }
+        }
+        if (all_pressed == true) return true;
+    }
+    return false;
+}
+
 void InputManager::GetInputEvents(InputEventArray& input_events){
     input_events = event_queue_;
     event_queue_.clear();
@@ -152,6 +181,65 @@ void InputManager::BindGameEvent(const Ogre::String& event, const ButtonList& bu
     bind_game_events_.push_back(info);
 }
 
+void InputManager::ScriptSetGameEventKeys(const char* event, const char* keys){
+    const Ogre::String event_name(event);
+    bind_game_events_.erase(
+      std::remove_if(
+        bind_game_events_.begin(), bind_game_events_.end(),
+        [&event_name](const BindGameEventInfo& bind){return bind.event == event_name;}
+      ),
+      bind_game_events_.end()
+    );
+    Ogre::StringVector combinations = Ogre::StringUtil::split(Ogre::String(keys), ",");
+    for (Ogre::String& combination : combinations){
+        Ogre::StringUtil::trim(combination);
+        if (combination.empty()) continue;
+        ButtonList key_codes;
+        if (ParseKeys(combination, key_codes) == false) BindGameEvent(event_name, key_codes);
+    }
+}
+
+std::string InputManager::ScriptGetGameEventKeys(const char* event) const{
+    const Ogre::String event_name(event);
+    std::string keys;
+    for (const BindGameEventInfo& bind : bind_game_events_){
+        if (bind.event != event_name) continue;
+        if (!keys.empty()) keys += ",";
+        for (unsigned int i = 0; i < bind.buttons.size(); ++ i){
+            if (i > 0) keys += "+";
+            keys += KeyToString(static_cast<OIS::KeyCode>(bind.buttons[i]));
+        }
+    }
+    return keys;
+}
+
+void InputManager::ScriptCaptureNextKey(){capture_next_key_ = true;}
+
+void InputManager::SnapshotDefaultGameEvents(){
+    default_game_event_keys_.clear();
+    for (const BindGameEventInfo& bind : bind_game_events_){
+        if (default_game_event_keys_.count(bind.event) == 0){
+            default_game_event_keys_[bind.event] = ScriptGetGameEventKeys(bind.event.c_str());
+        }
+    }
+}
+
+std::string InputManager::ScriptGetDefaultGameEventKeys(const char* event) const{
+    const auto it = default_game_event_keys_.find(Ogre::String(event));
+    return (it == default_game_event_keys_.end()) ? std::string() : it->second;
+}
+
+void InputManager::EnsureDefaultGameEvents(){
+    const struct{const char* event; OIS::KeyCode key;} defaults[] = {
+      {"Up", OIS::KC_UP}, {"Down", OIS::KC_DOWN}, {"Left", OIS::KC_LEFT}, {"Right", OIS::KC_RIGHT}
+    };
+    for (const auto& entry : defaults){
+        if (ScriptGetGameEventKeys(entry.event).empty()){
+            BindGameEvent(entry.event, ButtonList(1, static_cast<int>(entry.key)));
+        }
+    }
+}
+
 void InputManager::ActivateBinds(const int button) {
     std::vector< int > binds_indexes;
     for (unsigned int i = 0; i < binds_.size(); ++ i){

+ 76 - 0
src/core/InputManager.h

@@ -15,6 +15,7 @@
 
 #pragma once
 
+#include <map>
 #include <vector>
 #include <OgreSingleton.h>
 #include <OgreString.h>
@@ -151,6 +152,17 @@ class InputManager : public Manager, public Ogre::Singleton<InputManager>{
          */
         bool IsButtonPressed(int button) const;
 
+        /**
+         * Checks if a game event is active: all the keys of any of its bindings are being pressed.
+         *
+         * Use it, instead of checking keys, to poll the controls of the game, so the keys chosen
+         * in the controller settings are used. It is false while the console is open.
+         *
+         * @param[in] event The game event, for example "CONFIRM" or "walk_left".
+         * @return true if the event is active, false otherwise.
+         */
+        bool IsGameEventPressed(const Ogre::String& event) const;
+
         /**
          * Retrieves the current input events.
          *
@@ -184,6 +196,60 @@ class InputManager : public Manager, public Ogre::Singleton<InputManager>{
          */
         void BindGameEvent(const Ogre::String& event, const ButtonList& buttons);
 
+        /**
+         * Replaces all the keys bound to a game event.
+         *
+         * To be called from Lua scripts (controller setting of the config menu) and from the
+         * console command "set_game_event_keys".
+         *
+         * @param[in] event The game event.
+         * @param[in] keys Key names separated by commas (for example "Enter,Space"). Each one can
+         * be a combination of keys joined with '+' (for example "LCtrl+X"). If empty, the event is
+         * left with no keys.
+         */
+        void ScriptSetGameEventKeys(const char* event, const char* keys);
+
+        /**
+         * Gets the keys bound to a game event.
+         *
+         * @param[in] event The game event.
+         * @return Key names separated by commas, like the ones taken by ScriptSetGameEventKeys.
+         * Empty if the event has no keys.
+         */
+        std::string ScriptGetGameEventKeys(const char* event) const;
+
+        /**
+         * Makes the next key that is pressed be reported to the scripts as the game event
+         * "KEY_CAPTURE" (the name of the key is the third argument of on_button), instead of
+         * doing what it is bound to. To be called from Lua scripts, to let the player choose a
+         * key.
+         */
+        void ScriptCaptureNextKey();
+
+        /**
+         * Binds the directions of the menus (game events "Up", "Down", "Left" and "Right") to the
+         * arrow keys, if the configuration doesn't bind them.
+         */
+        void EnsureDefaultGameEvents();
+
+        /**
+         * Remembers the keys that every game event has now as its default keys.
+         *
+         * To be called once the configuration file was read, and before the keys chosen by the
+         * player are applied. See ScriptGetDefaultGameEventKeys.
+         */
+        void SnapshotDefaultGameEvents();
+
+        /**
+         * Gets the default keys of a game event: the ones it had when SnapshotDefaultGameEvents was
+         * called. To be called from Lua scripts, to restore the default keys.
+         *
+         * @param[in] event The game event.
+         * @return Key names separated by commas, like the ones of ScriptGetGameEventKeys. Empty if
+         * the event had no keys.
+         */
+        std::string ScriptGetDefaultGameEventKeys(const char* event) const;
+
         /**
          * Activates all bindings for a button.
          *
@@ -302,5 +368,15 @@ class InputManager : public Manager, public Ogre::Singleton<InputManager>{
          * List of game event bindings.
          */
         std::vector<BindGameEventInfo> bind_game_events_;
+
+        /**
+         * Indicates if the next key pressed must be captured for the scripts.
+         */
+        bool capture_next_key_ = false;
+
+        /**
+         * Default keys of the game events (see SnapshotDefaultGameEvents), by event.
+         */
+        std::map<Ogre::String, std::string> default_game_event_keys_;
 };
 

+ 20 - 0
src/core/InputManagerCommands.h

@@ -117,8 +117,28 @@ void CmdBindGameEvent(const Ogre::StringVector& params){
     }
 }
 
+/**
+ * Replaces all the keys bound to a game event.
+ *
+ * @param[in] params Parameters. Exactly three are required. The first one is the command name and
+ * it's not evaluated here. The second one is the game event, and the third one the keys separated
+ * by commas (a combination of keys is joined with '+'). Used by the controller settings file.
+ */
+void CmdSetGameEventKeys(const Ogre::StringVector& params){
+    if (params.size() != 3){
+        Console::getSingleton().AddTextToOutput(
+          "Usage: /set_game_event_keys \"<game event>\" \"<key1>[,<key2>...]\""
+        );
+        return;
+    }
+    InputManager::getSingleton().ScriptSetGameEventKeys(params[1].c_str(), params[2].c_str());
+}
+
 // TODO: Move this to InpuManager.cpp?
 void InputManager::InitCmd(){
+    ConfigCmdHandler::getSingleton().AddCommand(
+      "set_game_event_keys", "Replace the keys of a game event", "", CmdSetGameEventKeys, NULL
+    );
     ConfigCmdHandler::getSingleton().AddCommand(
       "bind", "Bind command to keys", "", CmdBind, NULL
     );

+ 85 - 48
src/core/Savemap.cpp

@@ -13,6 +13,7 @@
  * GNU General Public License for more details.
  */
 
+#include <boost/filesystem.hpp>
 #include <string>
 #include <iostream>
 #include <algorithm>
@@ -139,11 +140,12 @@ void Savemap::operator = (const Savemap &to_copy){
     control_ = to_copy.control_;
     for (int i = 0; i < 4; i ++){
         window_colours_[i][0] = to_copy.window_colours_[i][0];
-        window_colours_[i][1] = to_copy.window_colours_[i][0];
-        window_colours_[i][2] = to_copy.window_colours_[i][0];
+        window_colours_[i][1] = to_copy.window_colours_[i][1];
+        window_colours_[i][2] = to_copy.window_colours_[i][2];
     }
     for (int i = 0; i < 11; i ++) characters_[i] = to_copy.characters_[i];
     for (int i = 0; i < 3; i ++) party_[i] = to_copy.party_[i];
+    settings_ = to_copy.settings_;
 }
 
 void Savemap::SetData(const unsigned int bank, const unsigned int address, const int value){
@@ -253,8 +255,13 @@ void Savemap::SetLocation(
     location_.name = name;
 }
 
-void Savemap::SetSetting(const unsigned int key, const unsigned int value){
-    // TODO : Implements when settings are working.
+void Savemap::SetSetting(const std::string& key, const int value){settings_[key] = value;}
+
+bool Savemap::HasSetting(const std::string& key) const{return settings_.count(key) > 0;}
+
+int Savemap::GetSetting(const std::string& key) const{
+    const auto it = settings_.find(key);
+    return (it == settings_.end()) ? 0 : it->second;
 }
 
 void Savemap::SetCharacterInfo(
@@ -339,14 +346,14 @@ void Savemap::SetCharacterMateria(
 ){
     if (id < MAX_CHARACTERS && slot < MAX_EQUIP_SLOTS){
         if (weapon){
-            characters_[id].weapon.materia[slot].id = id;
+            characters_[id].weapon.materia[slot].id = materia;
             characters_[id].weapon.materia[slot].ap = ap;
             characters_[id].weapon.materia[slot].enemy_skill = false;
             for (int i = 0; i < MAX_ENEMY_SKILLS; i ++)
                 characters_[id].weapon.materia[slot].enemy_skill_learned[i] = false;
         }
         else{
-            characters_[id].armor.materia[slot].id = id;
+            characters_[id].armor.materia[slot].id = materia;
             characters_[id].armor.materia[slot].ap = ap;
             characters_[id].armor.materia[slot].enemy_skill = false;
             for (int i = 0; i < MAX_ENEMY_SKILLS; i ++)
@@ -485,11 +492,6 @@ std::string Savemap::GetLocationField(){return location_.field;}
 
 std::string Savemap::GetLocationName(){return location_.name;}
 
-int Savemap::GetSetting(const unsigned int key){
-    // TODO: Implement.
-    return 0;
-}
-
 int Savemap::GetCharacterCharId(const unsigned int id){
     if (id >= MAX_CHARACTERS) return -1;
     else return characters_[id].char_id;
@@ -790,8 +792,8 @@ void Savemap::Write(int slot, std::string file_name){
                         enemy_skill->SetAttribute("learned", true);
                         enemy_skills->LinkEndChild(enemy_skill.release());
                     }
-                    materia->LinkEndChild(enemy_skills.release());
                 }
+                materia->LinkEndChild(enemy_skills.release());
             }
             materias->LinkEndChild(materia.release());
         }
@@ -804,8 +806,8 @@ void Savemap::Write(int slot, std::string file_name){
         if (materia_stash_[i].id >= 0){
             std::unique_ptr<TiXmlElement> materia(new TiXmlElement("Materia"));
             materia->SetAttribute("slot", i);
-            materia->SetAttribute("id", materia_[i].id);
-            materia->SetAttribute("ap", materia_[i].ap);
+            materia->SetAttribute("id", materia_stash_[i].id);
+            materia->SetAttribute("ap", materia_stash_[i].ap);
             if (materia_stash_[i].enemy_skill){
                 materia->SetAttribute("enemy_skill", true);
                 std::unique_ptr<TiXmlElement> enemy_skills(new TiXmlElement("EnemySkill"));
@@ -816,15 +818,25 @@ void Savemap::Write(int slot, std::string file_name){
                         enemy_skill->SetAttribute("learned", true);
                         enemy_skills->LinkEndChild(enemy_skill.release());
                     }
-                    materia->LinkEndChild(enemy_skills.release());
                 }
+                materia->LinkEndChild(enemy_skills.release());
             }
             stash->LinkEndChild(materia.release());
         }
     }
     container->LinkEndChild(stash.release());
 
-    // TODO: Settings
+    // Settings (config menu). The window colours are saved apart.
+    if (!settings_.empty()){
+        std::unique_ptr<TiXmlElement> settings(new TiXmlElement("Settings"));
+        for (const auto& setting : settings_){
+            std::unique_ptr<TiXmlElement> element(new TiXmlElement("Setting"));
+            element->SetAttribute("name", setting.first);
+            element->SetAttribute("value", setting.second);
+            settings->LinkEndChild(element.release());
+        }
+        container->LinkEndChild(settings.release());
+    }
 
     // Characters
     std::unique_ptr<TiXmlElement> characters(new TiXmlElement("Characters"));
@@ -935,8 +947,8 @@ void Savemap::Write(int slot, std::string file_name){
                             enemy_skill->SetAttribute("learned", true);
                             character_weapon_enemy_skills->LinkEndChild(enemy_skill.release());
                         }
-                        materia->LinkEndChild(character_weapon_enemy_skills.release());
                     }
+                    materia->LinkEndChild(character_weapon_enemy_skills.release());
                 }
                 character_weapon_materia->LinkEndChild(materia.release());
             }
@@ -966,8 +978,8 @@ void Savemap::Write(int slot, std::string file_name){
                             enemy_skill->SetAttribute("learned", true);
                             character_armor_enemy_skills->LinkEndChild(enemy_skill.release());
                         }
-                        materia->LinkEndChild(character_armor_enemy_skills.release());
                     }
+                    materia->LinkEndChild(character_armor_enemy_skills.release());
                 }
                 character_armor_materia->LinkEndChild(materia.release());
             }
@@ -1006,7 +1018,14 @@ void Savemap::Write(int slot, std::string file_name){
 
     // Save
     xml.LinkEndChild(container.release());
-    xml.SaveFile(file_name);
+    // The folder of the saved games may not exist yet (a fresh installation): create it, or the
+    // game would not be saved.
+    const boost::filesystem::path path(file_name);
+    if (path.has_parent_path()){
+        boost::system::error_code error;
+        boost::filesystem::create_directories(path.parent_path(), error);
+    }
+    if (!xml.SaveFile(file_name)) LOG_ERROR("Can't write the saved game \"" + file_name + "\".");
     slot_ = slot;
     return;
 
@@ -1052,51 +1071,69 @@ void Savemap::Read(std::string file_name){
                     if (position == "top_left"){
                         colour = -1;
                         colour_elm->QueryIntAttribute("red", &colour);
-                        if (colour >= 0 && colour < MAX_COLOUR) window_colours_[0][0] = colour;
+                        if (colour >= 0 && colour <= MAX_COLOUR) window_colours_[0][0] = colour;
                         colour = -1;
                         colour_elm->QueryIntAttribute("green", &colour);
-                        if (colour >= 0 && colour < MAX_COLOUR) window_colours_[0][1] = colour;
+                        if (colour >= 0 && colour <= MAX_COLOUR) window_colours_[0][1] = colour;
                         colour = -1;
                         colour_elm->QueryIntAttribute("blue", &colour);
-                        if (colour >= 0 && colour < MAX_COLOUR) window_colours_[0][2] = colour;
+                        if (colour >= 0 && colour <= MAX_COLOUR) window_colours_[0][2] = colour;
                     }
                     else if (position == "top_right"){
                         colour = -1;
                         colour_elm->QueryIntAttribute("red", &colour);
-                        if (colour >= 0 && colour < MAX_COLOUR) window_colours_[1][0] = colour;
+                        if (colour >= 0 && colour <= MAX_COLOUR) window_colours_[1][0] = colour;
                         colour = -1;
                         colour_elm->QueryIntAttribute("green", &colour);
-                        if (colour >= 0 && colour < MAX_COLOUR) window_colours_[1][1] = colour;
+                        if (colour >= 0 && colour <= MAX_COLOUR) window_colours_[1][1] = colour;
                         colour = -1;
                         colour_elm->QueryIntAttribute("blue", &colour);
-                        if (colour >= 0 && colour < MAX_COLOUR) window_colours_[1][2] = colour;
+                        if (colour >= 0 && colour <= MAX_COLOUR) window_colours_[1][2] = colour;
                     }
                     else if (position == "bottom_right"){
                         colour = -1;
                         colour_elm->QueryIntAttribute("red", &colour);
-                        if (colour >= 0 && colour < MAX_COLOUR) window_colours_[2][0] = colour;
+                        if (colour >= 0 && colour <= MAX_COLOUR) window_colours_[2][0] = colour;
                         colour = -1;
                         colour_elm->QueryIntAttribute("green", &colour);
-                        if (colour >= 0 && colour < MAX_COLOUR) window_colours_[2][1] = colour;
+                        if (colour >= 0 && colour <= MAX_COLOUR) window_colours_[2][1] = colour;
                         colour = -1;
                         colour_elm->QueryIntAttribute("blue", &colour);
-                        if (colour >= 0 && colour < MAX_COLOUR) window_colours_[2][2] = colour;
+                        if (colour >= 0 && colour <= MAX_COLOUR) window_colours_[2][2] = colour;
                     }
-                    else if (position == "bottol_left"){
+                    else if (position == "bottom_left"){
                         colour = -1;
                         colour_elm->QueryIntAttribute("red", &colour);
-                        if (colour >= 0 && colour < MAX_COLOUR) window_colours_[3][0] = colour;
+                        if (colour >= 0 && colour <= MAX_COLOUR) window_colours_[3][0] = colour;
                         colour = -1;
                         colour_elm->QueryIntAttribute("green", &colour);
-                        if (colour >= 0 && colour < MAX_COLOUR) window_colours_[3][1] = colour;
+                        if (colour >= 0 && colour <= MAX_COLOUR) window_colours_[3][1] = colour;
                         colour = -1;
                         colour_elm->QueryIntAttribute("blue", &colour);
-                        if (colour >= 0 && colour < MAX_COLOUR) window_colours_[3][2] = colour;
+                        if (colour >= 0 && colour <= MAX_COLOUR) window_colours_[3][2] = colour;
                     }
                 }
                 colour_node = colour_node->NextSibling();
             }
         }
+        else if (node->Type() == TiXmlNode::TINYXML_ELEMENT && node->ValueStr() == "Settings"){
+            TiXmlNode* setting_node = node->FirstChild();
+            while (setting_node != nullptr){
+                if (
+                  setting_node->Type() == TiXmlNode::TINYXML_ELEMENT
+                  && setting_node->ValueStr() == "Setting"
+                ){
+                    std::string name;
+                    int value = 0;
+                    TiXmlElement* setting_elm = setting_node->ToElement();
+                    if (
+                      setting_elm->QueryStringAttribute("name", &name) == TIXML_SUCCESS
+                      && setting_elm->QueryIntAttribute("value", &value) == TIXML_SUCCESS
+                    ) settings_[name] = value;
+                }
+                setting_node = setting_node->NextSibling();
+            }
+        }
         else if (node->Type() == TiXmlNode::TINYXML_ELEMENT && node->ValueStr() == "Money"){
             int money = -1;
             node->ToElement()->QueryIntAttribute("quantity", &money);
@@ -1201,10 +1238,10 @@ void Savemap::Read(std::string file_name){
                     materia_elm->QueryIntAttribute("slot", &slot);
                     materia_elm->QueryIntAttribute("id", &id);
                     materia_elm->QueryIntAttribute("ap", &ap);
-                    materia_elm->QueryIntAttribute("enemy_skill", &ap);
+                    materia_elm->QueryIntAttribute("enemy_skill", &eskill);
                     if (
                       slot >= 0 && slot < MAX_MATERIA_SLOTS && id >= -1
-                      && ap >= 0 && (eskill == 0 | eskill == 1)
+                      && ap >= 0 && (eskill == 0 || eskill == 1)
                     ){
                         materia_[slot].id = id;
                         materia_[slot].ap = ap;
@@ -1230,10 +1267,10 @@ void Savemap::Read(std::string file_name){
                     materia_elm->QueryIntAttribute("slot", &slot);
                     materia_elm->QueryIntAttribute("id", &id);
                     materia_elm->QueryIntAttribute("ap", &ap);
-                    materia_elm->QueryIntAttribute("enemy_skill", &ap);
+                    materia_elm->QueryIntAttribute("enemy_skill", &eskill);
                     if (
                       slot >= 0 && slot < MAX_STASH_SLOTS && id >= -1
-                      && ap >= 0 && (eskill == 0 | eskill == 1)
+                      && ap >= 0 && (eskill == 0 || eskill == 1)
                     ){
                         materia_stash_[slot].id = id;
                         materia_stash_[slot].ap = ap;
@@ -1441,19 +1478,19 @@ void Savemap::Read(std::string file_name){
                                           && mat_node->ValueStr() == "Materia"
                                         ){
                                             int slot = -1;
-                                            int id = -2;
+                                            int materia_id = -2;
                                             int ap = -1;
                                             int eskill = 0;
                                             TiXmlElement* mat_elm = mat_node->ToElement();
                                             mat_elm->QueryIntAttribute("slot", &slot);
-                                            mat_elm->QueryIntAttribute("id", &id);
+                                            mat_elm->QueryIntAttribute("id", &materia_id);
                                             mat_elm->QueryIntAttribute("ap", &ap);
-                                            mat_elm->QueryIntAttribute("enemy_skill", &ap);
+                                            mat_elm->QueryIntAttribute("enemy_skill", &eskill);
                                             if (
-                                              slot >= 0 && slot < MAX_EQUIP_SLOTS && id >= -1
-                                              && ap >= 0 && (eskill == 0 | eskill == 1)
+                                              slot >= 0 && slot < MAX_EQUIP_SLOTS && materia_id >= -1
+                                              && ap >= 0 && (eskill == 0 || eskill == 1)
                                             ){
-                                                characters_[id].weapon.materia[slot].id = id;
+                                                characters_[id].weapon.materia[slot].id = materia_id;
                                                 characters_[id].weapon.materia[slot].ap = ap;
                                                 characters_[id].weapon.materia[slot].enemy_skill
                                                   = (eskill == 1);
@@ -1483,19 +1520,19 @@ void Savemap::Read(std::string file_name){
                                           && mat_node->ValueStr() == "Materia"
                                         ){
                                             int slot = -1;
-                                            int id = -2;
+                                            int materia_id = -2;
                                             int ap = -1;
                                             int eskill = 0;
                                             TiXmlElement* mat_elm = mat_node->ToElement();
                                             mat_elm->QueryIntAttribute("slot", &slot);
-                                            mat_elm->QueryIntAttribute("id", &id);
+                                            mat_elm->QueryIntAttribute("id", &materia_id);
                                             mat_elm->QueryIntAttribute("ap", &ap);
-                                            mat_elm->QueryIntAttribute("enemy_skill", &ap);
+                                            mat_elm->QueryIntAttribute("enemy_skill", &eskill);
                                             if (
-                                              slot >= 0 && slot < MAX_EQUIP_SLOTS && id >= -1
-                                              && ap >= 0 && (eskill == 0 | eskill == 1)
+                                              slot >= 0 && slot < MAX_EQUIP_SLOTS && materia_id >= -1
+                                              && ap >= 0 && (eskill == 0 || eskill == 1)
                                             ){
-                                                characters_[id].armor.materia[slot].id = id;
+                                                characters_[id].armor.materia[slot].id = materia_id;
                                                 characters_[id].armor.materia[slot].ap = ap;
                                                 characters_[id].armor.materia[slot].enemy_skill
                                                   = (eskill == 1);

+ 23 - 112
src/core/Savemap.h

@@ -15,6 +15,7 @@
 
 #pragma once
 
+#include <map>
 #include <string>
 #include "common/TypeDefine.h"
 
@@ -211,11 +212,26 @@ class Savemap{
         /**
          * Sets a setting value in the savemap.
          *
-         * @param[in] key Setting key.
+         * @param[in] key Name of the setting.
          * @param[in] value Setting value.
-         * @todo Does nothing, implement when settings are working.
          */
-        void SetSetting(const unsigned int key, const unsigned int value);
+        void SetSetting(const std::string& key, const int value);
+
+        /**
+         * Checks if the savemap has a setting.
+         *
+         * @param[in] key Name of the setting.
+         * @return True if the setting was saved. Savemaps from older versions have none.
+         */
+        bool HasSetting(const std::string& key) const;
+
+        /**
+         * Gets the value of a setting.
+         *
+         * @param[in] key Name of the setting.
+         * @return The value of the setting, or 0 if the savemap doesn't have it.
+         */
+        int GetSetting(const std::string& key) const;
 
         /**
          * Sets a character basic information in the savemap.
@@ -537,15 +553,6 @@ class Savemap{
          */
         std::string GetLocationName();
 
-        /**
-         * Retrieves a setting.
-         *
-         * @param[in] key Setting key.
-         * @return The setting value.
-         * @todo Implement and document properly.
-         */
-        int GetSetting(const unsigned int key);
-
         /**
          * Retrieves the char ID of a character.
          *
@@ -937,105 +944,9 @@ class Savemap{
         static constexpr unsigned int BANK_ADDRESS_COUNT = 256;
 
         /**
-         * MAx colour component value.
+         * Max colour component value (they are stored in a byte).
          */
-        static constexpr unsigned int MAX_COLOUR = 254;
-
-
-        /**
-         * Game settings.
-         */
-        struct Settings{
-
-            /**
-             * Magic sorting methods.
-             */
-            enum class MagicOrder{
-
-                /**
-                 * Restore-Attack-Indirect.
-                 */
-                RAI,
-
-                /**
-                 * Restore-Indirect-Attack.
-                 */
-                RIA,
-
-                /**
-                 * Attack-Indirect-Restore.
-                 */
-                AIR,
-
-                /**
-                 * Attack-Restore-Indirect.
-                 */
-                ARI,
-
-                /**
-                 * Indirect-Restore-Attack.
-                 */
-                IRA,
-
-                /**
-                 * Indirect-Attack-Restore.
-                 */
-                IAR,
-            };
-
-            /**
-             * ATB modes.
-             */
-            enum class AtbMode{
-
-                /**
-                 * Active battle.
-                 */
-                ACTIVE,
-
-                /**
-                 * Mixed mode.
-                 */
-                RECOMMENDED,
-
-                /**
-                 * Paused mode.
-                 */
-                WAIT
-            };
-
-            /**
-             * Battle speed.
-             */
-            unsigned int battle_speed;
-
-            /**
-             * Battle message speed.
-             */
-            unsigned int battle_msg_speed;
-
-            /**
-             * Field message speed
-             */
-            unsigned int msg_speed;
-
-            /**
-             * Magic order.
-             */
-            MagicOrder order;
-
-            /**
-             * ATB mode.
-             */
-            AtbMode atb_mode;
-
-            /**
-             * Indicates if the battle help window is to be shown.
-             */
-            bool batle_help;
-
-            // TODO: Key mappings, when implemented.
-        };
+        static constexpr unsigned int MAX_COLOUR = 255;
 
         /**
          * Location information.
@@ -1373,9 +1284,9 @@ class Savemap{
         Location location_;
 
         /**
-         * Game settings.
+         * Settings of the game (config menu), by name. The scripts decide which ones there are.
          */
-        Settings settings_;
+        std::map<std::string, int> settings_;
 
         /**
          * Memory banks.

+ 14 - 8
src/core/SavemapHandler.cpp

@@ -179,9 +179,21 @@ void SavemapHandler::SetLocation(
     current_savemap_->SetLocation(x, y, z, triangle, angle, std::string(field), std::string(name));
 }
 
-void SavemapHandler::SetSetting(const unsigned int key, const unsigned int value){
+void SavemapHandler::SetSetting(const char* key, const int value){
     if (current_savemap_ == nullptr) current_savemap_ = new Savemap();
-    current_savemap_->SetSetting(key, value);
+    current_savemap_->SetSetting(std::string(key), value);
+}
+
+bool SavemapHandler::IsSlotSettingSet(const unsigned int slot, const char* key){
+    if (slot >= MAX_SAVE_SLOTS) return false;
+    if (savemaps_read_ == false) ReadSavemaps();
+    return saved_savemaps_[slot]->HasSetting(std::string(key));
+}
+
+int SavemapHandler::GetSlotSetting(const unsigned int slot, const char* key){
+    if (slot >= MAX_SAVE_SLOTS) return 0;
+    if (savemaps_read_ == false) ReadSavemaps();
+    return saved_savemaps_[slot]->GetSetting(std::string(key));
 }
 
 void SavemapHandler::SetCharacterInfo(
@@ -393,12 +405,6 @@ std::string SavemapHandler::GetSlotLocationName(const unsigned int slot){
     return saved_savemaps_[slot]->GetLocationName();
 }
 
-int SavemapHandler::GetSlotSetting(const unsigned int slot, const unsigned int key){
-    if (slot >= MAX_SAVE_SLOTS) return 0;
-    if (savemaps_read_ == false) ReadSavemaps();
-    return saved_savemaps_[slot]->GetSetting(key);
-}
-
 int SavemapHandler::GetSlotCharacterCharId(const unsigned int slot, const unsigned int id){
     if (slot >= MAX_SAVE_SLOTS) return 0;
     if (savemaps_read_ == false) ReadSavemaps();

+ 19 - 11
src/core/SavemapHandler.h

@@ -252,7 +252,25 @@ class SavemapHandler : public Ogre::Singleton<SavemapHandler>{
          * @param[in] value Setting value.
          * @todo Does nothing, implement when settings are working.
          */
-        void SetSetting(const unsigned int key, const unsigned int value);
+        void SetSetting(const char* key, const int value);
+
+        /**
+         * Checks if a saved savemap has a setting.
+         *
+         * @param[in] slot The slot to read from.
+         * @param[in] key Name of the setting.
+         * @return True if the slot has the setting. False if it doesn't, or the slot is invalid.
+         */
+        bool IsSlotSettingSet(const unsigned int slot, const char* key);
+
+        /**
+         * Gets a setting from a saved savemap.
+         *
+         * @param[in] slot The slot to read from.
+         * @param[in] key Name of the setting.
+         * @return The value of the setting, or 0 if the slot doesn't have it.
+         */
+        int GetSlotSetting(const unsigned int slot, const char* key);
 
         /**
          * Sets a character basic information in the current savemap.
@@ -607,16 +625,6 @@ class SavemapHandler : public Ogre::Singleton<SavemapHandler>{
          */
         std::string GetSlotLocationName(const unsigned int slot);
 
-        /**
-         * Retrieves a setting from a saved savemap.
-         *
-         * @param[in] slot The slot to read from.
-         * @param[in] key Setting key.
-         * @return The setting value.
-         * @todo Implement and document properly.
-         */
-        int GetSlotSetting(const unsigned int slot, const unsigned int key);
-
         /**
          * Retrieves the char ID of a character from a saved savemap.
          *

+ 4 - 11
src/core/ScriptManager.cpp

@@ -87,18 +87,11 @@ void ScriptManager::Input(const VGears::Event& event){
     if (
       (event.type == VGears::ET_KEY_PRESS || event.type == VGears::ET_KEY_REPEAT_WAIT)
       && (
+        // Only the keys that scripts read by themselves reach them as raw keys: the letters and the
+        // space bar, to type in the name menu and for the debug keys of the battle. Everything else
+        // goes through the game events of config.cfg, so it can be remapped.
         // TODO: Do this the other way: block keys NOT TO pass to scripts.
-        event.param1 == OIS::KC_RETURN
-        || event.param1 == OIS::KC_ESCAPE
-        || event.param1 == OIS::KC_SPACE
-        || event.param1 == OIS::KC_LCONTROL
-        || event.param1 == OIS::KC_LEFT
-        || event.param1 == OIS::KC_RIGHT
-        || event.param1 == OIS::KC_DOWN
-        || event.param1 == OIS::KC_UP
-        || event.param1 == OIS::KC_PGUP
-        || event.param1 == OIS::KC_PGDOWN
-        || event.param1 == OIS::KC_BACK
+        event.param1 == OIS::KC_SPACE
         || event.param1 == OIS::KC_A || event.param1 == OIS::KC_B || event.param1 == OIS::KC_C
         || event.param1 == OIS::KC_D || event.param1 == OIS::KC_E || event.param1 == OIS::KC_F
         || event.param1 == OIS::KC_G || event.param1 == OIS::KC_H || event.param1 == OIS::KC_I

+ 21 - 3
src/core/ScriptManagerBinds.h

@@ -24,6 +24,7 @@
 #include "EntityManager.h"
 #include "BattleManager.h"
 #include "AudioManager.h"
+#include "InputManager.h"
 #include "SavemapHandler.h"
 #include "Timer.h"
 #include "UiManager.h"
@@ -203,6 +204,7 @@ void ScriptManager::InitBinds(){
       "unset_player_entity", &EntityManager::ScriptUnsetPlayerEntity,
       "player_lock", &EntityManager::ScriptPlayerLock,
       "is_player_locked", &EntityManager::IsPlayerLocked,
+      "set_player_default_run", &EntityManager::SetPlayerDefaultRun,
       "random_encounters_on", &EntityManager::SetRandomEncounters,
       "set_encounter_table", &EntityManager::ScriptSetEncounterTable,
       "roll_encounter_battle", &EntityManager::ScriptRollEncounterBattle,
@@ -335,7 +337,18 @@ void ScriptManager::InitBinds(){
       "get_track_id", &AudioManager::ScriptGetTrack,
       "get_battle_track_id", &AudioManager::ScriptGetBattleTrack,
       "set_battle_track_id", &AudioManager::ScriptSetBattleTrack,
-      "play_music_once", &AudioManager::ScriptPlayMusicOnce
+      "play_music_once", &AudioManager::ScriptPlayMusicOnce,
+      "set_music_volume", &AudioManager::ScriptSetMusicVolume,
+      "set_sound_volume", &AudioManager::ScriptSetSoundVolume
+    );
+
+    // Commands for the input manager (controller settings).
+    lua.new_usertype<InputManager>(
+      "InputManager",
+      "set_game_event_keys", &InputManager::ScriptSetGameEventKeys,
+      "get_game_event_keys", &InputManager::ScriptGetGameEventKeys,
+      "get_default_game_event_keys", &InputManager::ScriptGetDefaultGameEventKeys,
+      "capture_next_key", &InputManager::ScriptCaptureNextKey
     );
 
     // Commands for the camera manager.
@@ -379,6 +392,9 @@ void ScriptManager::InitBinds(){
         // Registered under the same name in the original bindings.
         &SavemapHandler::GetSlotWindowCornerColourComponent
       ),
+      "get_slot_window_colour", &SavemapHandler::GetSlotWindowCornerColourComponent,
+      "is_slot_setting_set", &SavemapHandler::IsSlotSettingSet,
+      "get_slot_setting", &SavemapHandler::GetSlotSetting,
       "get_slot_money", &SavemapHandler::GetSlotMoney,
       "get_slot_game_time", &SavemapHandler::GetSlotGameTime,
       "get_slot_countdown_time", &SavemapHandler::GetSlotCountdownTime,
@@ -403,7 +419,6 @@ void ScriptManager::InitBinds(){
       "get_slot_location_angle", &SavemapHandler::GetSlotLocationAngle,
       "get_slot_location_field", &SavemapHandler::GetSlotLocationField,
       "get_slot_location_name", &SavemapHandler::GetSlotLocationName,
-      "get_slot_setting", &SavemapHandler::GetSlotSetting,
       "get_slot_character_char_id", &SavemapHandler::GetSlotCharacterCharId,
       "get_slot_character_name", &SavemapHandler::GetSlotCharacterName,
       "get_slot_character_level", &SavemapHandler::GetSlotCharacterLevel,
@@ -527,6 +542,7 @@ void ScriptManager::InitBinds(){
       ),
       "set_default_animation", &UiWidget::ScriptSetDefaultAnimation,
       "set_colour", &UiWidget::SetColour,
+      "set_colours", &UiWidget::SetColours,
       "set_alpha", &UiWidget::SetAlpha,
       "set_x", &UiWidget::SetX,
       "set_y", &UiWidget::SetY,
@@ -544,7 +560,8 @@ void ScriptManager::InitBinds(){
     // UI manager commands. Use to get a specific widget.
     lua.new_usertype<UiManager>(
       "UiManager",
-      "get_widget", &UiManager::ScriptGetWidget
+      "get_widget", &UiManager::ScriptGetWidget,
+      "set_window_colours", &UiManager::SetWindowColours
     );
 
     // Timer command. To show a in-game timer.
@@ -584,6 +601,7 @@ void ScriptManager::InitBinds(){
     );
 
     // Register all command handlers
+    lua["input_manager"] = InputManager::getSingletonPtr();
     lua["camera_manager"] = CameraManager::getSingletonPtr();
     lua["entity_manager"] = EntityManager::getSingletonPtr();
     lua["battle_manager"] = BattleManager::getSingletonPtr();

+ 25 - 0
src/core/UiManager.cpp

@@ -123,6 +123,31 @@ UiWidget* UiManager::GetWidget(const Ogre::String& name){
 
 UiWidget* UiManager::ScriptGetWidget(const char* name){return GetWidget(Ogre::String(name));}
 
+void UiManager::SetWindowColours(
+  const float r1, const float g1, const float b1, const float r2, const float g2, const float b2,
+  const float r3, const float g3, const float b3, const float r4, const float g4, const float b4
+){
+    const float colours[12] = {r1, g1, b1, r2, g2, b2, r3, g3, b3, r4, g4, b4};
+    for (int i = 0; i < 12; ++ i) window_colours_[i] = colours[i];
+    window_colours_set_ = true;
+    for (unsigned int i = 0; i < widgets_.size(); ++ i) ApplyWindowColours(widgets_[i]);
+}
+
+void UiManager::ApplyWindowColours(UiWidget* widget){
+    if (widget == nullptr || window_colours_set_ == false) return;
+    if (widget->IsWindowBackground()){
+        widget->SetColours(
+          window_colours_[0], window_colours_[1], window_colours_[2],
+          window_colours_[3], window_colours_[4], window_colours_[5],
+          window_colours_[6], window_colours_[7], window_colours_[8],
+          window_colours_[9], window_colours_[10], window_colours_[11]
+        );
+    }
+    for (unsigned int i = 0; i < widget->GetNumberOfChildren(); ++ i){
+        ApplyWindowColours(widget->GetChild(i));
+    }
+}
+
 void UiManager::renderQueueStarted(
   Ogre::uint8 queueGroupId, const Ogre::String& invocation, bool& skipThisInvocation
 ){

+ 45 - 0
src/core/UiManager.h

@@ -139,6 +139,40 @@ class UiManager :
          */
         UiWidget* ScriptGetWidget(const char* name);
 
+        /**
+         * Sets the colours of the background of every window.
+         *
+         * They are applied to all the existing window backgrounds (widgets marked with
+         * UiWidget::SetWindowBackground), and to the ones created later.
+         *
+         * @param[in] r1 Red component for the top-left corner (0-1).
+         * @param[in] g1 Green component for the top-left corner (0-1).
+         * @param[in] b1 Blue component for the top-left corner (0-1).
+         * @param[in] r2 Red component for the top-right corner (0-1).
+         * @param[in] g2 Green component for the top-right corner (0-1).
+         * @param[in] b2 Blue component for the top-right corner (0-1).
+         * @param[in] r3 Red component for the bottom-right corner (0-1).
+         * @param[in] g3 Green component for the bottom-right corner (0-1).
+         * @param[in] b3 Blue component for the bottom-right corner (0-1).
+         * @param[in] r4 Red component for the bottom-left corner (0-1).
+         * @param[in] g4 Green component for the bottom-left corner (0-1).
+         * @param[in] b4 Blue component for the bottom-left corner (0-1).
+         */
+        void SetWindowColours(
+          const float r1, const float g1, const float b1,
+          const float r2, const float g2, const float b2,
+          const float r3, const float g3, const float b3,
+          const float r4, const float g4, const float b4
+        );
+
+        /**
+         * Applies the window colours to a widget if it is a window background, and to its
+         * children. Does nothing until the colours have been set.
+         *
+         * @param[in] widget The widget.
+         */
+        void ApplyWindowColours(UiWidget* widget);
+
         /**
          * Updates the render queue.
          *
@@ -200,4 +234,15 @@ class UiManager :
          * List of widgets.
          */
         std::vector<UiWidget*> widgets_;
+
+        /**
+         * Colours of the windows (RGB of the top-left, top-right, bottom-right and bottom-left
+         * corners). Only used if window_colours_set_.
+         */
+        float window_colours_[12];
+
+        /**
+         * Indicates if the window colours were set.
+         */
+        bool window_colours_set_ = false;
 };

+ 4 - 0
src/core/UiWidget.cpp

@@ -567,6 +567,10 @@ void UiWidget::SetColours(
     SetUpdateTransformation();
 }
 
+void UiWidget::SetWindowBackground(const bool background){window_background_ = background;}
+
+bool UiWidget::IsWindowBackground() const{return window_background_;}
+
 void UiWidget::SetAlpha(const float a){
     colour_1_.a = a;
     colour_2_.a = a;

+ 22 - 0
src/core/UiWidget.h

@@ -502,6 +502,23 @@ class UiWidget{
           const float r4, const float g4, const float b4
         );
 
+        /**
+         * Marks the widget as the background of a window.
+         *
+         * Window backgrounds all share the colours the player chooses in the config menu, see
+         * UiManager::SetWindowColours.
+         *
+         * @param[in] background True to mark it as a window background.
+         */
+        void SetWindowBackground(const bool background);
+
+        /**
+         * Checks if the widget is the background of a window.
+         *
+         * @return True if the widget is a window background.
+         */
+        bool IsWindowBackground() const;
+
         /**
          * Sets the widget transparency.
          *
@@ -604,6 +621,11 @@ class UiWidget{
          */
         bool visible_;
 
+        /**
+         * Indicates if the widget is the background of a window, which uses the window colours.
+         */
+        bool window_background_ = false;
+
         /**
          * Top-left corner colour.
          */

+ 4 - 0
src/core/XmlScreenFile.cpp

@@ -131,6 +131,10 @@ void XmlScreenFile::LoadScreenRecursive(
                         Ogre::Vector3 colour = GetVector3(node, "colour", Ogre::Vector3(1, 1, 1));
                         widget2->SetColour(colour.x, colour.y, colour.z);
                     }
+                    if (GetBool(node, "window_background", false) == true){
+                        widget2->SetWindowBackground(true);
+                        UiManager::getSingleton().ApplyWindowColours(widget2);
+                    }
                     float alpha = GetFloat(node, "alpha", 1);
                     widget2->SetAlpha(alpha);
                     Ogre::String align = GetString(node, "align");

+ 35 - 5
src/installer/KernelDataInstaller.cpp

@@ -176,7 +176,11 @@ int KernelDataInstaller::ReadAttacks(){
     File attacks_desc_file = kernel_.ExtractGZip(KERNEL_ATTACK_DESCRIPTIONS);
     int attack_count = 0;
 
-    for (int i = 0; i < 128; i ++){
+    limit_attacks_.clear();
+
+    // The kernel has data for 128 attacks, but names and descriptions for 256. The last 128 are the
+    // limit break techniques, which the characters refer to by ID (128 and up).
+    for (int i = 0; i < 256; i ++){
 
         AttackData data;
         int name_offset;
@@ -226,6 +230,20 @@ int KernelDataInstaller::ReadAttacks(){
         }
         boost::replace_all(data.description, "\"", "'");
 
+        // Limit techniques only have name and description. Both start with control codes, drop
+        // everything before the first letter.
+        if (i >= 128){
+            for (std::string* text : {&data.name, &data.description}){
+                size_t first = text->find_first_of(
+                  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
+                if (first == std::string::npos) text->clear();
+                else text->erase(0, first);
+            }
+            data.id = i;
+            limit_attacks_.push_back(data);
+            continue;
+        }
+
         // Read data from the item data section.
         data.accuracy = attacks_file.readU8();
         data.impact_effect = attacks_file.readU8();
@@ -356,6 +374,13 @@ void KernelDataInstaller::WriteAttacks(std::string file_name){
         }
         file << "}\n}\n";
     }
+    // Limit techniques: only name and description are known.
+    for (AttackData attack : limit_attacks_){
+        file << "Game.Attacks[" << attack.id << "] = {\n"
+          << "    name = \"" << attack.name << "\",\n"
+          << "    description = \"" << attack.description << "\"\n"
+          << "}\n";
+    }
     file.close();
 }
 
@@ -1007,7 +1032,7 @@ void KernelDataInstaller::WriteWeapons(std::string file_name){
         file << "},\n    stats = {";
         for (StatBonus s : weapon.stat_bonus){
             file << "\n        " << GetBasicStatName(s.stat) << " = "
-              << static_cast<int>(s.stat) << ",";
+              << static_cast<int>(s.bonus) << ",";
         }
         if (weapon.stat_bonus.size() > 0) file << "\n    },\n";
         else file << "},\n";
@@ -1200,7 +1225,7 @@ void KernelDataInstaller::WriteArmors(std::string file_name){
         file << "},\n    stats = {";
         for (StatBonus s : armor.stat_bonus){
             file << "\n        " << GetBasicStatName(s.stat) << " = "
-              << static_cast<int>(s.stat) << ",";
+              << static_cast<int>(s.bonus) << ",";
         }
         if (armor.stat_bonus.size() > 0) file << "\n    },\n";
         else file << "},\n";
@@ -1395,11 +1420,16 @@ void KernelDataInstaller::WriteAccessories(std::string file_name){
         file
           << "},\n"
           << "    elements = {";
-        for (int s : accessory.elements) file << s << ", ";
+        // Elements and their resistance mode (0 absorbs, 1 nullifies, 2 halves), as in armors.
+        for (int s : accessory.elements){
+            file << "\n        {id = " << s << ", mode = "
+              << static_cast<int>(accessory.element_defense_mode) << "},";
+        }
+        if (accessory.elements.size() > 0) file << "\n    ";
         file << "},\n    stats = {";
         for (StatBonus s : accessory.stat_bonus){
             file << "\n        " << GetBasicStatName(s.stat) << " = "
-              << static_cast<int>(s.stat) << ",";
+              << static_cast<int>(s.bonus) << ",";
         }
         if (accessory.stat_bonus.size() > 0) file << "\n    },\n";
         else file << "},\n";

+ 2 - 0
src/installer/KernelDataInstaller.h

@@ -2562,6 +2562,8 @@ class KernelDataInstaller{
          * Attacks read from the kernel.
          */
         std::vector<AttackData> attacks_;
+        /** Limit techniques (attack IDs 128 and up). Only name and description are filled. */
+        std::vector<AttackData> limit_attacks_;
 
         /**
          * Character info read from the kernel.

+ 25 - 3
src/installer/decompiler/field/instruction/FieldPartyInstruction.cpp

@@ -52,9 +52,9 @@ void FieldPartyInstruction::ProcessInst(
         case OPCODES::PRTYP: code_gen->WriteTodo(md.GetEntityName(), "PRTYP"); break;
         case OPCODES::PRTYM: code_gen->WriteTodo(md.GetEntityName(), "PRTYM"); break;
         case OPCODES::PRTYE: ProcessPRTYE(code_gen); break;
-        case OPCODES::MMBUD: code_gen->WriteTodo(md.GetEntityName(), "MMBUD"); break;
-        case OPCODES::MMBLK: code_gen->WriteTodo(md.GetEntityName(), "MMBLK"); break;
-        case OPCODES::MMBUK: code_gen->WriteTodo(md.GetEntityName(), "MMBUK"); break;
+        case OPCODES::MMBUD: ProcessMMBUD(code_gen); break;
+        case OPCODES::MMBLK: ProcessMMBLK(code_gen); break;
+        case OPCODES::MMBUK: ProcessMMBUK(code_gen); break;
         case OPCODES::CHGLD: ProcessCHGLD(code_gen); break;
         default:
             code_gen->AddOutputLine(FieldCodeGenerator::FormatInstructionNotImplemented(
@@ -141,6 +141,28 @@ void FieldPartyInstruction::ProcessPRTYE(CodeGenerator* code_gen){
     ).str());
 }
 
+void FieldPartyInstruction::ProcessMMBUD(CodeGenerator* code_gen){
+    // Param 0 is the character, param 1 is 0 to hide it in the PHS and anything else to show it.
+    code_gen->AddOutputLine((
+      boost::format("set_character_enable(%1%, %2%)") % params_[0]->GetString()
+        % (params_[1]->GetUnsigned() != 0 ? "true" : "false")
+    ).str());
+}
+
+void FieldPartyInstruction::ProcessMMBLK(CodeGenerator* code_gen){
+    // Locks the character in the party: it can't be swapped in the PHS.
+    code_gen->AddOutputLine((
+      boost::format("set_party_lock(%1%, true)") % params_[0]->GetString()
+    ).str());
+}
+
+void FieldPartyInstruction::ProcessMMBUK(CodeGenerator* code_gen){
+    // Unlocks the character, so it can be swapped in the PHS again.
+    code_gen->AddOutputLine((
+      boost::format("set_party_lock(%1%, false)") % params_[0]->GetString()
+    ).str());
+}
+
 void FieldPartyInstruction::ProcessCHGLD(CodeGenerator* code_gen){
     code_gen->AddOutputLine((
       boost::format("Inventory.money_to_banks(%1%, %2%, %3%, %4%)")

+ 15 - 0
src/installer/decompiler/field/instruction/FieldPartyInstruction.h

@@ -163,6 +163,21 @@ class FieldPartyInstruction : public KernelCallInstruction{
 
         void ProcessPRTYE(CodeGenerator* code_gen);
 
+        /**
+         * Processes a MMBUD opcode: makes a character available or unavailable in the PHS.
+         */
+        void ProcessMMBUD(CodeGenerator* code_gen);
+
+        /**
+         * Processes a MMBLK opcode: locks a character in the party.
+         */
+        void ProcessMMBLK(CodeGenerator* code_gen);
+
+        /**
+         * Processes a MMBUK opcode: unlocks a character in the party.
+         */
+        void ProcessMMBUK(CodeGenerator* code_gen);
+
         /**
          * Processes a CHGLD opcode.
          *

+ 9 - 0
src/main.cpp

@@ -13,6 +13,7 @@
  * GNU General Public License for more details.
  */
 
+#include <fstream>
 #include <OgreRoot.h>
 #include <OgreConfigFile.h>
 #include <OgreArchiveManager.h>
@@ -163,6 +164,14 @@ int main(int argc, char *argv[]){
         // Execute the configuration file to locad values.
         ConfigFile config;
         config.Execute("./data/config/config.cfg");
+        input_manager->EnsureDefaultGameEvents();
+        // The keys of the configuration file are the default ones.
+        input_manager->SnapshotDefaultGameEvents();
+        // Controller settings chosen by the player, if any. They replace the default keys.
+        {
+            std::ifstream controls("./controls.cfg");
+            if (controls.good()) config.Execute("./controls.cfg");
+        }
 
         // Initialize UID and run it's scripts.
         ui_manager->Initialise();

+ 7 - 6
src/modules/worldmap/WorldmapModule.cpp

@@ -1134,8 +1134,7 @@ namespace VGears {
 
         const bool highwind_active = active_entity == highwind_entity_;
       const bool vehicle_active = current_vehicle_ != VehicleType::ON_FOOT;
-        const bool cancel_pressed
-          = InputManager::getSingleton().IsButtonPressed(OIS::KC_ESCAPE);
+        const bool cancel_pressed = InputManager::getSingleton().IsGameEventPressed("CANCEL");
         if (vehicle_active && cancel_pressed && !cancel_was_pressed_){
             ScriptManager::getSingleton().ScriptRequest(
               ScriptManager::ENTITY, active_entity->GetName().c_str(), "unboard", 0
@@ -1155,10 +1154,12 @@ namespace VGears {
             // Nothing: no movement.
         }
         else{
-            if (InputManager::getSingleton().IsButtonPressed(OIS::KC_LEFT)) movement.x -= 1.0f;
-            if (InputManager::getSingleton().IsButtonPressed(OIS::KC_RIGHT)) movement.x += 1.0f;
-            if (InputManager::getSingleton().IsButtonPressed(OIS::KC_UP)) movement.z -= 1.0f;
-            if (InputManager::getSingleton().IsButtonPressed(OIS::KC_DOWN)) movement.z += 1.0f;
+            // The game events of the movement, so the keys of the controller settings are used.
+            InputManager& input = InputManager::getSingleton();
+            if (input.IsGameEventPressed("walk_left")) movement.x -= 1.0f;
+            if (input.IsGameEventPressed("walk_right")) movement.x += 1.0f;
+            if (input.IsGameEventPressed("walk_up")) movement.z -= 1.0f;
+            if (input.IsGameEventPressed("walk_down")) movement.z += 1.0f;
         }
         bool is_walking = false;
         if (!movement.isZeroLength()){