magic_menu.lua 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. if UiContainer == nil then UiContainer = {} end
  2. local ROOT = "MagicMenu.Container."
  3. --- Rows visible in the list, and abilities in each row.
  4. local ROWS = 9
  5. local COLUMNS = 4
  6. --- Categories of the abilities, in the order of the options of the menu.
  7. local KINDS = { "magic", "summon", "e_skill" }
  8. --- Colour of the abilities that can be used from the menu, and of the rest.
  9. local COLOUR_USABLE = { 1.0, 1.0, 1.0 }
  10. local COLOUR_UNUSABLE = { 0.45, 0.45, 0.35 }
  11. --- Number of uses that means a summon can be used without limit.
  12. local UNLIMITED = 255
  13. --- Attacks (by ID) that can be used from the menu, out of battle. The rest are shown in gray.
  14. --
  15. -- The data of the attacks has a condition for the menu, but it is not reliable (Life has none, and
  16. -- some spells that can't be cast out of battle have one), so the list is explicit.
  17. local MENU_ATTACKS = {
  18. [0] = true, -- Cure
  19. [1] = true, -- Cure2
  20. [2] = true, -- Cure3
  21. [7] = true, -- Life
  22. [8] = true, -- Life2
  23. }
  24. --- Creates the entry of the list for an attack.
  25. --
  26. -- @param id ID of the attack.
  27. -- @param uses Times it can be used (summons), or nil.
  28. -- @param mp MP the character has now. The attacks that cost more are not usable.
  29. local function create_entry(id, uses, mp)
  30. local attack = Game.Attacks[id]
  31. if attack == nil then return nil end
  32. return {
  33. id = id,
  34. name = attack.name,
  35. description = attack.description or "",
  36. mp = attack.mp or 0,
  37. uses = uses,
  38. usable = MENU_ATTACKS[id] == true and (attack.mp or 0) <= mp,
  39. }
  40. end
  41. --- Splits a list in rows of at most COLUMNS elements.
  42. local function split_in_rows(list)
  43. local rows = {}
  44. for index, item in ipairs(list) do
  45. local row = math.floor((index - 1) / COLUMNS) + 1
  46. rows[row] = rows[row] or {}
  47. rows[row][#rows[row] + 1] = item
  48. end
  49. return rows
  50. end
  51. --- The magic menu.
  52. --
  53. -- At first the player chooses what to see (magic, summons or enemy skills) in the box at the right,
  54. -- and the abilities of that kind are shown below in a grid of four columns. Magic has a row for each
  55. -- materia, with the spells it gives. Summons and enemy skills fill the rows in order. OK goes into
  56. -- the grid, where the cursor moves over the abilities. The description of the one that is highlighted is shown in the help
  57. -- window, and the MP it needs (and the times it can be used, for summons) in the box at the right.
  58. --
  59. -- The abilities that can be cast from the menu (MENU_ATTACKS) are white, as long as the character
  60. -- has the MP they need. The rest are gray.
  61. --
  62. -- OK on one of them opens a window with the party to choose who receives it. The spell heals or
  63. -- revives and the MP are spent, or it fails (Error sound) and nothing is spent.
  64. -- TODO: Added-Ability is always "Nothing." The ability linked to the materia isn't calculated.
  65. UiContainer.MagicMenu = UiContainer.Menu.new{
  66. id = "magic",
  67. parent = "main",
  68. --- ID of the character being shown.
  69. char_id = -1,
  70. --- Rows of the grid of the character, by kind (magic, summon, e_skill). Each row is a list of
  71. -- up to COLUMNS entries.
  72. rows = { magic = {}, summon = {}, e_skill = {} },
  73. --- Kind of abilities whose grid is shown.
  74. kind = "magic",
  75. --- Position of the cursor in the grid: row and column of the highlighted ability, and the first
  76. -- row that is visible.
  77. row = 1,
  78. column = 1,
  79. first_row = 1,
  80. --- Cursor over the options: magic, summon and enemy skills.
  81. category_cursor = UiContainer.Cursor.new{
  82. widget = ROOT .. "Side.Options.Cursor",
  83. total = #KINDS,
  84. on_move = function(position)
  85. UiContainer.MagicMenu:show_category(position)
  86. end,
  87. },
  88. --- Party slots shown in the window to choose the target of a spell, by position of the cursor.
  89. target_slots = {},
  90. --- Cursor over the characters of the party, to choose who receives the spell.
  91. target_cursor = UiContainer.Cursor.new{
  92. total = 1,
  93. on_move = function(position)
  94. UiContainer.MagicMenu:update_target_cursor(position)
  95. end,
  96. },
  97. states = {
  98. --- Choosing what to see.
  99. [""] = {
  100. cursor = function(self) return self.category_cursor end,
  101. axis = "vertical",
  102. confirm = function(self)
  103. if #self.rows[self.kind] == 0 then return false end
  104. self:set_browsing(true)
  105. self.row, self.column, self.first_row = 1, 1, 1
  106. self:update_cursor()
  107. self:set_state("list")
  108. end,
  109. cancel = function(self)
  110. script:request_end_sync(Script.UI, "MagicMenu", "hide", 0)
  111. end,
  112. },
  113. --- Moving over the abilities of the list.
  114. list = {
  115. confirm = function(self)
  116. local entry = self.rows[self.kind][self.row][self.column]
  117. if not entry.usable then return false end
  118. self:open_targets()
  119. self:set_state("target")
  120. end,
  121. buttons = {
  122. [Config.CONTROLS.UP] = function(self, event) self:move_cursor(-1, 0) end,
  123. [Config.CONTROLS.DOWN] = function(self, event) self:move_cursor(1, 0) end,
  124. [Config.CONTROLS.LEFT] = function(self, event) self:move_cursor(0, -1) end,
  125. [Config.CONTROLS.RIGHT] = function(self, event) self:move_cursor(0, 1) end,
  126. },
  127. cancel = function(self)
  128. self:set_browsing(false)
  129. self:set_state("")
  130. end,
  131. },
  132. --- Choosing the character that receives the spell.
  133. target = {
  134. cursor = function(self) return self.target_cursor end,
  135. axis = "vertical",
  136. confirm = function(self, cursor)
  137. return self:cast_spell(cursor:index())
  138. end,
  139. cancel = function(self)
  140. self:close_targets()
  141. self:set_state("list")
  142. end,
  143. },
  144. },
  145. --- Opens the window to choose the character that receives the spell.
  146. open_targets = function(self)
  147. self.target_slots = {}
  148. for slot = 1, Party.MAX do
  149. if Party[slot] ~= nil and Characters[Party[slot]] ~= nil then
  150. self.target_slots[#self.target_slots + 1] = slot
  151. end
  152. end
  153. self.target_cursor.total = #self.target_slots
  154. self:populate_targets()
  155. ui_manager:get_widget(ROOT .. "Targets"):set_visible(true)
  156. self.target_cursor:set(1)
  157. end,
  158. --- Closes the window to choose the target.
  159. close_targets = function(self)
  160. ui_manager:get_widget(ROOT .. "Targets"):set_visible(false)
  161. end,
  162. --- Shows the data of the party in the window to choose the target.
  163. populate_targets = function(self)
  164. for slot = 1, Party.MAX do
  165. local path = ROOT .. "Targets.Slot" .. tostring(slot)
  166. local id = Party[slot]
  167. if id ~= nil and Characters[id] ~= nil then
  168. UiContainer.populate_character_data(path, Characters[id])
  169. ui_manager:get_widget(path .. ".Portrait"):set_image("characters/" .. tostring(id) .. ".png")
  170. else
  171. UiContainer.populate_character_data(path, nil)
  172. end
  173. end
  174. end,
  175. --- Puts the cursor of the targets on a character.
  176. -- @param position Position in target_slots.
  177. update_target_cursor = function(self, position)
  178. local slot = self.target_slots[position]
  179. if slot ~= nil then
  180. ui_manager:get_widget(ROOT .. "Targets.Cursor"):set_default_animation("Position" .. tostring(slot))
  181. end
  182. end,
  183. --- Casts the highlighted spell on a character of the party.
  184. --
  185. -- @param position Position of the target in target_slots.
  186. -- @return False if the spell can't be cast (Error sound).
  187. cast_spell = function(self, position)
  188. local entry = self.rows[self.kind][self.row][self.column]
  189. local target = Party[self.target_slots[position]]
  190. if not Characters.cast_menu_spell(self.char_id, entry.id, target) then return false end
  191. -- HP and MP changed.
  192. self:populate_targets()
  193. UiContainer.populate_character_data(ROOT .. "Character", Characters[self.char_id])
  194. self:refresh_usable()
  195. -- Without MP for another one, go back to the list.
  196. if not entry.usable then
  197. self:close_targets()
  198. self:set_state("list")
  199. end
  200. end,
  201. --- Updates which spells can be cast, as the MP of the character changed.
  202. refresh_usable = function(self)
  203. local mp = Characters[self.char_id].stats.mp.current
  204. for _, rows in pairs(self.rows) do
  205. for _, row in ipairs(rows) do
  206. for _, entry in ipairs(row) do
  207. entry.usable = MENU_ATTACKS[entry.id] == true and entry.mp <= mp
  208. end
  209. end
  210. end
  211. self:populate_list()
  212. end,
  213. --- Builds the grids of abilities of the character.
  214. build_rows = function(self)
  215. local abilities = Characters.get_abilities(self.char_id)
  216. local mp = Characters[self.char_id].stats.mp.current
  217. local rows = { magic = {}, summon = {}, e_skill = {} }
  218. -- Magic: one row for each materia (more rows if it gives more than COLUMNS spells).
  219. for _, group in ipairs(abilities.magic_groups) do
  220. local entries = {}
  221. for _, id in ipairs(group) do entries[#entries + 1] = create_entry(id, nil, mp) end
  222. for _, row in ipairs(split_in_rows(entries)) do rows.magic[#rows.magic + 1] = row end
  223. end
  224. local summons = {}
  225. for _, summon in ipairs(abilities.summon) do
  226. summons[#summons + 1] = create_entry(summon.id, summon.times, mp)
  227. end
  228. rows.summon = split_in_rows(summons)
  229. local e_skills = {}
  230. for _, id in ipairs(abilities.e_skill) do e_skills[#e_skills + 1] = create_entry(id, nil, mp) end
  231. rows.e_skill = split_in_rows(e_skills)
  232. self.rows = rows
  233. end,
  234. --- Shows the grid of a kind of abilities, with the cursor out of it.
  235. --
  236. -- @param position Position of the option (1 magic, 2 summon, 3 enemy skill).
  237. show_category = function(self, position)
  238. self.kind = KINDS[position]
  239. self.row, self.column, self.first_row = 1, 1, 1
  240. self:populate_list()
  241. self:update_scroll()
  242. ui_manager:get_widget(ROOT .. "Help.Help"):set_text("")
  243. end,
  244. --- Shows the abilities of the rows that are visible.
  245. populate_list = function(self)
  246. local rows = self.rows[self.kind]
  247. for visible_row = 1, ROWS do
  248. local row = rows[self.first_row + visible_row - 1]
  249. for column = 1, COLUMNS do
  250. local widget = ui_manager:get_widget(
  251. ROOT .. "List.R" .. tostring(visible_row) .. "C" .. tostring(column))
  252. local entry = row ~= nil and row[column] or nil
  253. if entry == nil then
  254. widget:set_visible(false)
  255. else
  256. -- The colour has to be set before the text, as it applies to it when it's set.
  257. local colour = entry.usable and COLOUR_USABLE or COLOUR_UNUSABLE
  258. widget:set_colour(colour[1], colour[2], colour[3])
  259. widget:set_text(entry.name)
  260. widget:set_visible(true)
  261. end
  262. end
  263. end
  264. end,
  265. --- Moves the scroll bar of the list. It is only shown if the grid doesn't fit.
  266. update_scroll = function(self)
  267. local count = #self.rows[self.kind]
  268. local visible = count > ROWS
  269. ui_manager:get_widget(ROOT .. "List.ScrollBg"):set_visible(visible)
  270. local scroll = ui_manager:get_widget(ROOT .. "List.Scroll")
  271. scroll:set_visible(visible)
  272. if visible then
  273. local fraction = (self.first_row - 1) / (count - ROWS)
  274. -- Between 3 pixels from the top and 21 from the bottom of the list.
  275. scroll:set_y(fraction * 100, 3 - fraction * 24)
  276. end
  277. end,
  278. --- Moves the cursor over the grid.
  279. --
  280. -- Up and down keep the column, or go to the last ability if the row has fewer.
  281. --
  282. -- @param delta_row -1 to go up, 1 to go down.
  283. -- @param delta_column -1 to go left, 1 to go right.
  284. move_cursor = function(self, delta_row, delta_column)
  285. local rows = self.rows[self.kind]
  286. local row, column = self.row, self.column + delta_column
  287. if delta_column ~= 0 then
  288. -- Left and right stay in the row.
  289. if column < 1 or column > #rows[row] then return end
  290. else
  291. row = row + delta_row
  292. if row < 1 or row > #rows then
  293. UiContainer.play_sound("Error")
  294. return
  295. end
  296. column = math.min(column, #rows[row])
  297. end
  298. self.row, self.column = row, column
  299. UiContainer.play_sound("Cursor")
  300. -- Scroll if the cursor leaves the visible rows.
  301. local first_row = self.first_row
  302. if row < first_row then first_row = row
  303. elseif row > first_row + ROWS - 1 then first_row = row - ROWS + 1 end
  304. if first_row ~= self.first_row then
  305. self.first_row = first_row
  306. self:populate_list()
  307. self:update_scroll()
  308. end
  309. self:update_cursor()
  310. end,
  311. --- Puts the cursor on the highlighted ability and shows its description and cost.
  312. update_cursor = function(self)
  313. local visible_row = self.row - self.first_row + 1
  314. ui_manager:get_widget(ROOT .. "List.Cursor"):set_default_animation(
  315. "Position" .. tostring(visible_row) .. "-" .. tostring(self.column))
  316. local entry = self.rows[self.kind][self.row][self.column]
  317. ui_manager:get_widget(ROOT .. "Help.Help"):set_text(entry.description)
  318. ui_manager:get_widget(ROOT .. "Side.Cost.MpValue"):set_text(UiContainer.zero_pad_value(entry.mp, 3))
  319. local uses = ui_manager:get_widget(ROOT .. "Side.Cost.Uses")
  320. uses:set_visible(entry.uses ~= nil)
  321. if entry.uses ~= nil then
  322. local text = (entry.uses >= UNLIMITED) and "---" or UiContainer.zero_pad_value(entry.uses, 3)
  323. ui_manager:get_widget(ROOT .. "Side.Cost.Uses.Value"):set_text(text)
  324. end
  325. end,
  326. --- Switches between choosing what to see and moving over the grid.
  327. --
  328. -- @param browsing True when the cursor is in the grid.
  329. set_browsing = function(self, browsing)
  330. ui_manager:get_widget(ROOT .. "Side.Options"):set_visible(not browsing)
  331. ui_manager:get_widget(ROOT .. "Side.Cost"):set_visible(browsing)
  332. ui_manager:get_widget(ROOT .. "Character.Added"):set_visible(browsing)
  333. ui_manager:get_widget(ROOT .. "List.Cursor"):set_visible(browsing)
  334. if not browsing then
  335. ui_manager:get_widget(ROOT .. "Help.Help"):set_text("")
  336. end
  337. end,
  338. --- Opens the magic menu for UiContainer.current_character_id.
  339. show = function(self)
  340. self.char_id = UiContainer.current_character_id
  341. self:activate()
  342. self:build_rows()
  343. local character = Characters[self.char_id]
  344. UiContainer.populate_character_data(ROOT .. "Character", character)
  345. ui_manager:get_widget(ROOT .. "Character.Portrait"):set_image("characters/" .. tostring(self.char_id) .. ".png")
  346. self:set_browsing(false)
  347. self.category_cursor:set(1)
  348. ui_manager:get_widget("MagicMenu"):set_visible(true)
  349. return 0
  350. end,
  351. --- Closes the magic menu and goes back to the main menu.
  352. hide = function(self)
  353. ui_manager:get_widget("MagicMenu"):set_visible(false)
  354. self:deactivate()
  355. return 0
  356. end,
  357. }