Browse Source

Menu overhaul: shared menu library, new menus, settings, and fixes

Menu framework
- New menu_lib.lua with Cursor, ListCursor and a Menu state machine that
  replace the nested current_menu/current_submenu ladders. Shared helpers:
  grid_buttons, set_bar, set_limit_bar, exp_progress, cursor_memory. A menu
  that gives the control back runs the on_resume hook of the receiver, so
  the main menu refreshes after its submenus close.
- Pause, begin, save and main menus use the framework.
- Maximum HP and MP shown in menus now include equipment and materia.

New menus
- Status: three pages (stats and equipment, elemental, status effects).
  Character effects come from the new character_effects.lua.
- Order: toggles the row of a character and moves its portrait.
- Limit: Set and Check modes, per-level technique lists and a confirmation
  to change the level. The installer now reads the limit technique names and
  descriptions (attack IDs 128+) from the kernel.
- Quit: confirmation window that closes the game.
- PHS: swap characters between the party and the reserve. Locked
  characters can't move, the party must be complete, and the reserve keeps
  the places of the portraits. The field opcodes MMBUD, MMBLK and MMBUK are
  now decompiled to set_character_enable and set_party_lock.
- Magic: magic (one row per materia), summons and enemy skills in a grid with
  descriptions, MP cost and uses. Cure and Life spells can be cast on a party
  member from the menu. New character_abilities.lua computes the abilities
  from the equipped materia.
- Config: window color, sound, controller, cursor, ATB, battle speed and
  messages, movement, camera angle and magic order.
  - Window color: four corners with RGB sliders, applied to every window.
    The engine marks window backgrounds (window_background) and exposes
    ui_manager:set_window_colours and widget:set_colours.
  - Sound: music and effects volume (0-4), applied at once through
    audio_manager:set_music_volume and set_sound_volume. The music volume
    also controls the audio of the movies.
  - Controller: the player can assign a key to each action, including the
    directions. Changes are applied and saved in controls.cfg, which the game
    runs after config.cfg, when the window is closed and the keys are valid.
    New input_manager functions to set, get and capture keys, the console
    command set_game_event_keys, and the menu directions are now game events.
  - Cursor memory (menus and battle), ATB modes in battle, and walk or run by
    default (entity_manager:set_player_default_run) are working. Camera angle
    is stored only, as the battle camera doesn't move yet.

Fixes
- Item menu: wrong item selected and crash on the first item.
- Equip menu: list off-by-one, and weapons lost after one swap.
- Materia saving: character IDs were stored as materia IDs, and armor
  materia used the weapon data.
- Savemap: fix the XML loader (materia indexed by materia ID, enemy skill
  read into AP), saving the stash and enemy skill elements, and the copy of
  the window colours. The maximum colour is now 255.
- Fix the text colour of the options of the config menu, set_party and the
  party add and remove functions, and the field names of the character
  enable and lock.
- Accessory elements now keep their mode, like armor.

Cleanup
- Remove the dead prototype and unused data and texts.

Known gaps: the settings are not saved in the savemap yet, Added-Ability is
always "Nothing.", and the controller can't remap the keys of other modules
that read raw keys.
Iñigo Valentin 11 hours ago
parent
commit
57f8cc72f1
57 changed files with 4699 additions and 668 deletions
  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. 42 10
      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. 136 125
      data/data/scripts/menu/begin_menu.lua
  20. 628 0
      data/data/scripts/menu/config_menu.lua
  21. 20 13
      data/data/scripts/menu/equip_menu.lua
  22. 24 31
      data/data/scripts/menu/item_menu.lua
  23. 235 0
      data/data/scripts/menu/limit_menu.lua
  24. 393 0
      data/data/scripts/menu/magic_menu.lua
  25. 182 161
      data/data/scripts/menu/main_menu.lua
  26. 1 0
      data/data/scripts/menu/materia_menu.lua
  27. 20 33
      data/data/scripts/menu/menu_helpers.lua
  28. 467 0
      data/data/scripts/menu/menu_lib.lua
  29. 31 44
      data/data/scripts/menu/pause_menu.lua
  30. 313 0
      data/data/scripts/menu/phs_menu.lua
  31. 49 0
      data/data/scripts/menu/quit_menu.lua
  32. 43 97
      data/data/scripts/menu/save_menu.lua
  33. 215 0
      data/data/scripts/menu/status_menu.lua
  34. 22 21
      data/data/scripts/save.lua
  35. 31 31
      data/data/scripts/system.lua
  36. 114 2
      data/data/texts/english/menu.xml
  37. 23 0
      src/core/AudioManager.cpp
  38. 35 0
      src/core/AudioManager.h
  39. 4 1
      src/core/EntityManager.cpp
  40. 16 0
      src/core/EntityManager.h
  41. 72 0
      src/core/InputManager.cpp
  42. 65 0
      src/core/InputManager.h
  43. 20 0
      src/core/InputManagerCommands.h
  44. 30 27
      src/core/Savemap.cpp
  45. 2 2
      src/core/Savemap.h
  46. 0 4
      src/core/ScriptManager.cpp
  47. 18 2
      src/core/ScriptManagerBinds.h
  48. 25 0
      src/core/UiManager.cpp
  49. 45 0
      src/core/UiManager.h
  50. 4 0
      src/core/UiWidget.cpp
  51. 22 0
      src/core/UiWidget.h
  52. 4 0
      src/core/XmlScreenFile.cpp
  53. 32 2
      src/installer/KernelDataInstaller.cpp
  54. 2 0
      src/installer/KernelDataInstaller.h
  55. 25 3
      src/installer/decompiler/field/instruction/FieldPartyInstruction.cpp
  56. 15 0
      src/installer/decompiler/field/instruction/FieldPartyInstruction.h
  57. 9 0
      src/main.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

+ 42 - 10
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,
 
@@ -336,7 +344,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 +366,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 +390,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 +404,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 +425,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 +449,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 +462,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 +483,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 +526,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 +569,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 +609,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 +2680,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 +2710,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 +2734,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 +3195,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

+ 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,
+}

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

@@ -0,0 +1,628 @@
+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.
+-- TODO: Apply the volumes (apply_volumes), the movement (apply_movement) and the window colours
+-- when the settings are loaded.
+-- TODO: Save and load all of them with the savemap (see the TODO about the settings in save.lua and
+-- Savemap.cpp).
+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
+
+--- 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,
+
+    --- 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

+ 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,
+}

+ 22 - 21
data/data/scripts/save.lua

@@ -14,7 +14,13 @@ 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
+    -- Window colours of the config menu: red, green and blue (0-255) of the top-left, top-right,
+    -- bottom-right and bottom-left corners.
+    local colours = MenuSettings.config.window_colours
+    savemap_manager:set_window_colours(
+        colours[1][1], colours[1][2], colours[1][3], colours[2][1], colours[2][2], colours[2][3],
+        colours[3][1], colours[3][2], colours[3][3], colours[4][1], colours[4][2], colours[4][3]
+    )
     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 +41,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 +52,7 @@ 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
+    -- TODO: Save the rest of the settings of the config menu (MenuSettings.config).
     for i = 0, 11 do
         if Characters[i] ~= nil and Characters[i].char_id ~= nil then
             local c = Characters[i]
@@ -63,23 +69,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
@@ -109,7 +109,8 @@ load_game = function(slot)
             Banks[b][a] = savemap_manager:get_slot_data(slot, b, a)
         end
     end
-    -- TODO: Window colours
+    -- TODO: Window colours and the rest of the settings of the config menu (MenuSettings.config),
+    -- then apply the colours with ui_manager:set_window_colours().
     Inventory.money = savemap_manager:get_slot_money(slot)
     -- TODO: Game time
     -- TODO: Countdown

+ 31 - 31
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.
@@ -625,6 +622,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 +656,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 +675,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 +689,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 +723,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.
                  *

+ 4 - 1
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.");

+ 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.
          */

+ 72 - 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;
@@ -152,6 +165,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){

+ 65 - 0
src/core/InputManager.h

@@ -15,6 +15,7 @@
 
 #pragma once
 
+#include <map>
 #include <vector>
 #include <OgreSingleton.h>
 #include <OgreString.h>
@@ -184,6 +185,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 +357,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
     );

+ 30 - 27
src/core/Savemap.cpp

@@ -139,8 +139,8 @@ 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];
@@ -339,14 +339,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 ++)
@@ -790,8 +790,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 +804,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 +816,18 @@ 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
+    // TODO: Settings. Save all the options of the config menu (MenuSettings.config in the scripts):
+    // window colours, sound, controller, cursor mode, ATB mode, battle speed, battle message
+    // speed, field message speed, movement, camera angle and magic order. Savemap::Settings and the
+    // window colours already exist, but they are not written to the XML nor read back.
 
     // Characters
     std::unique_ptr<TiXmlElement> characters(new TiXmlElement("Characters"));
@@ -935,8 +938,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 +969,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());
             }
@@ -1201,10 +1204,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 +1233,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 +1444,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 +1486,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);

+ 2 - 2
src/core/Savemap.h

@@ -937,9 +937,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;
+        static constexpr unsigned int MAX_COLOUR = 255;
 
 
         /**

+ 0 - 4
src/core/ScriptManager.cpp

@@ -92,10 +92,6 @@ void ScriptManager::Input(const VGears::Event& event){
         || 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

+ 18 - 2
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.
@@ -527,6 +540,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 +558,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 +599,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");

+ 32 - 2
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();
 }
 
@@ -1395,7 +1420,12 @@ 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) << " = "

+ 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();