phs_menu.lua 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. if UiContainer == nil then UiContainer = {} end
  2. local ROOT = "PhsMenu.Container."
  3. --- Slots of the party (left) and of the reserve (right, a grid of 3x3).
  4. local PARTY_SLOTS = 3
  5. local RESERVE_COLUMNS = 3
  6. local RESERVE_ROWS = 3
  7. local RESERVE_SLOTS = RESERVE_COLUMNS * RESERVE_ROWS
  8. --- The cursor goes over "cells": 1-3 are the party slots, 4-12 the reserve slots.
  9. local TOTAL_CELLS = PARTY_SLOTS + RESERVE_SLOTS
  10. --- Highest character id the PHS shows (Cid). Young Cloud and Sephiroth can't be chosen, and the
  11. -- nine characters fit in the party (3) and the reserve (9).
  12. local MAX_CHARACTER_ID = 8
  13. local function is_party_cell(cell)
  14. return cell <= PARTY_SLOTS
  15. end
  16. --- Gets the cell the cursor goes to when a direction is pressed.
  17. --
  18. -- @param cell Current cell.
  19. -- @param button A direction button.
  20. -- @return The new cell, or nil if the cursor can't move that way.
  21. local function neighbour(cell, button)
  22. local UP, DOWN = Config.CONTROLS.UP, Config.CONTROLS.DOWN
  23. local LEFT, RIGHT = Config.CONTROLS.LEFT, Config.CONTROLS.RIGHT
  24. if is_party_cell(cell) then
  25. if button == UP then
  26. return (cell - 2) % PARTY_SLOTS + 1
  27. elseif button == DOWN then
  28. return cell % PARTY_SLOTS + 1
  29. elseif button == RIGHT then
  30. -- First column of the reserve, in the same row.
  31. return PARTY_SLOTS + (cell - 1) * RESERVE_COLUMNS + 1
  32. end
  33. return nil
  34. end
  35. local index = cell - PARTY_SLOTS - 1
  36. local column = index % RESERVE_COLUMNS
  37. local row = math.floor(index / RESERVE_COLUMNS)
  38. if button == LEFT then
  39. if column > 0 then return cell - 1 end
  40. return math.min(row + 1, PARTY_SLOTS)
  41. elseif button == RIGHT then
  42. if column < RESERVE_COLUMNS - 1 then return cell + 1 end
  43. elseif button == UP then
  44. return PARTY_SLOTS + ((row - 1) % RESERVE_ROWS) * RESERVE_COLUMNS + column + 1
  45. elseif button == DOWN then
  46. return PARTY_SLOTS + ((row + 1) % RESERVE_ROWS) * RESERVE_COLUMNS + column + 1
  47. end
  48. return nil
  49. end
  50. --- Is a character locked in its place? Locked characters can't be moved or replaced.
  51. local function is_locked(char_id)
  52. return char_id ~= nil and Characters[char_id].locked == true
  53. end
  54. --- Number of characters in the party.
  55. local function party_count()
  56. local count = 0
  57. for slot = 1, PARTY_SLOTS do
  58. if Party[slot] ~= nil then count = count + 1 end
  59. end
  60. return count
  61. end
  62. --- Handlers of the directional buttons.
  63. local function direction_buttons()
  64. local buttons = {}
  65. for _, button in ipairs({
  66. Config.CONTROLS.UP, Config.CONTROLS.DOWN, Config.CONTROLS.LEFT, Config.CONTROLS.RIGHT
  67. }) do
  68. buttons[button] = function(self, event)
  69. local target = neighbour(self.cursor:index(), button)
  70. if target ~= nil then
  71. UiContainer.play_sound("Cursor")
  72. self.cursor:set(target)
  73. end
  74. end
  75. end
  76. return buttons
  77. end
  78. --- The PHS menu: changes the characters of the party.
  79. --
  80. -- The party is on the left and the rest of the available characters on the right. A character is
  81. -- picked with OK, and then OK on another place swaps both:
  82. -- * party <-> party: changes the order (or moves the character to an empty slot).
  83. -- * party <-> reserve: the character leaves the party and the other one (if any) takes its place.
  84. -- * reserve <-> reserve: not allowed.
  85. -- Locked characters can't be moved or replaced. The party can only be left once it has three
  86. -- characters, or all the available ones if there are fewer.
  87. UiContainer.PhsMenu = UiContainer.Menu.new{
  88. id = "phs",
  89. parent = "main",
  90. --- Cell the cursor is on (see TOTAL_CELLS).
  91. cursor = UiContainer.Cursor.new{
  92. widget = ROOT .. "Cursor",
  93. total = TOTAL_CELLS,
  94. sound = false,
  95. on_move = function(cell)
  96. UiContainer.PhsMenu:update_info(cell)
  97. end,
  98. },
  99. --- Cell of the picked character, or nil.
  100. held = nil,
  101. --- IDs of the available characters that aren't in the party, in the order they are shown.
  102. reserve = {},
  103. states = {
  104. [""] = {
  105. buttons = direction_buttons(),
  106. confirm = function(self)
  107. return self:on_confirm(self.cursor:index())
  108. end,
  109. cancel = function(self)
  110. if self.held ~= nil then
  111. self:set_held(nil)
  112. return
  113. end
  114. if not self:is_party_complete() then return false end
  115. script:request_end_sync(Script.UI, "PhsMenu", "hide", 0)
  116. end,
  117. },
  118. },
  119. --- Gets the character in a cell.
  120. -- @return Character ID, or nil if the cell is empty.
  121. get_character = function(self, cell)
  122. if is_party_cell(cell) then return Party[cell] end
  123. return self.reserve[cell - PARTY_SLOTS]
  124. end,
  125. --- Puts a character in a cell, or empties it.
  126. set_character = function(self, cell, char_id)
  127. if is_party_cell(cell) then
  128. Party[cell] = char_id
  129. else
  130. self.reserve[cell - PARTY_SLOTS] = char_id
  131. end
  132. end,
  133. --- Number of characters in the reserve. The reserve can have empty cells between them.
  134. reserve_count = function(self)
  135. local count = 0
  136. for slot = 1, RESERVE_SLOTS do
  137. if self.reserve[slot] ~= nil then count = count + 1 end
  138. end
  139. return count
  140. end,
  141. --- Fills the reserve with the available characters that aren't in the party.
  142. --
  143. -- Characters are packed from the first cell, in order. From then on they keep their cell
  144. -- while the party is being formed (see swap).
  145. refresh_reserve = function(self)
  146. self.reserve = {}
  147. for id = 0, MAX_CHARACTER_ID do
  148. local character = Characters[id]
  149. if character ~= nil and character.enabled == true then
  150. local in_party = false
  151. for slot = 1, PARTY_SLOTS do
  152. if Party[slot] == id then in_party = true end
  153. end
  154. if not in_party then
  155. self.reserve[#self.reserve + 1] = id
  156. end
  157. end
  158. end
  159. end,
  160. --- Has the party all the characters it can have?
  161. --
  162. -- Three are needed, unless there are fewer characters available.
  163. is_party_complete = function(self)
  164. local available = party_count() + self:reserve_count()
  165. return party_count() >= math.min(PARTY_SLOTS, available)
  166. end,
  167. --- Picks a character, or drops the one that was picked.
  168. -- @param cell Cell that was selected. nil to drop.
  169. set_held = function(self, cell)
  170. self.held = cell
  171. local held = ui_manager:get_widget(ROOT .. "Held")
  172. if cell == nil then
  173. held:set_visible(false)
  174. else
  175. held:set_default_animation("Position" .. tostring(cell))
  176. held:set_visible(true)
  177. end
  178. end,
  179. --- OK was pressed on a cell.
  180. -- @return false if the action isn't allowed.
  181. on_confirm = function(self, cell)
  182. local character = self:get_character(cell)
  183. if self.held == nil then
  184. -- Pick a character.
  185. if character == nil or is_locked(character) then return false end
  186. self:set_held(cell)
  187. return
  188. end
  189. -- Picking the same one again drops it.
  190. if self.held == cell then
  191. self:set_held(nil)
  192. return
  193. end
  194. if not self:swap(self.held, cell) then return false end
  195. self:set_held(nil)
  196. self:populate()
  197. end,
  198. --- Swaps the characters of two cells.
  199. -- @return false if it isn't allowed.
  200. swap = function(self, from, to)
  201. if not is_party_cell(from) and not is_party_cell(to) then return false end
  202. local a = self:get_character(from)
  203. local b = self:get_character(to)
  204. if is_locked(a) or is_locked(b) then return false end
  205. -- Both cells exchange their contents, so nobody in the reserve changes place.
  206. self:set_character(from, b)
  207. self:set_character(to, a)
  208. set_party(Party[1], Party[2], Party[3])
  209. -- TODO: If the leader (first slot) changed, change the player entity of the field too.
  210. return true
  211. end,
  212. --- Moves the characters of the party to the first slots, with no gaps between them.
  213. compact_party = function(self)
  214. local members = {}
  215. for slot = 1, PARTY_SLOTS do
  216. if Party[slot] ~= nil then members[#members + 1] = Party[slot] end
  217. end
  218. set_party(members[1], members[2], members[3])
  219. end,
  220. --- Shows the characters of the party and of the reserve.
  221. populate = function(self)
  222. self:update_info(self.cursor:index())
  223. for slot = 1, PARTY_SLOTS do
  224. local path = ROOT .. "Party.Slot" .. tostring(slot)
  225. local id = Party[slot]
  226. if id ~= nil then
  227. UiContainer.populate_character_data(path, Characters[id])
  228. ui_manager:get_widget(path .. ".Portrait"):set_image("characters/" .. tostring(id) .. ".png")
  229. else
  230. UiContainer.populate_character_data(path, nil)
  231. end
  232. end
  233. for slot = 1, RESERVE_SLOTS do
  234. local path = ROOT .. "Reserve.Slot" .. tostring(slot)
  235. local id = self.reserve[slot]
  236. ui_manager:get_widget(path):set_visible(id ~= nil)
  237. if id ~= nil then
  238. ui_manager:get_widget(path .. ".Portrait"):set_image("characters/" .. tostring(id) .. ".png")
  239. end
  240. end
  241. end,
  242. --- Shows the details of the character under the cursor in the top right window.
  243. --
  244. -- Only for the reserve: the characters of the party already show them next to their portrait.
  245. update_info = function(self, cell)
  246. local path = ROOT .. "Info.Character"
  247. local id = nil
  248. if not is_party_cell(cell) then id = self:get_character(cell) end
  249. if id ~= nil then
  250. UiContainer.populate_character_data(path, Characters[id])
  251. else
  252. UiContainer.populate_character_data(path, nil)
  253. end
  254. end,
  255. --- Opens the PHS menu.
  256. show = function(self)
  257. self:activate()
  258. -- Empty slots may hold an invalid ID (e.g. -1). Leave them as nil.
  259. for slot = 1, PARTY_SLOTS do
  260. if Party[slot] ~= nil and Characters[Party[slot]] == nil then Party[slot] = nil end
  261. end
  262. set_party(Party[1], Party[2], Party[3])
  263. -- Whoever is in the party is available, even if no script enabled them.
  264. for slot = 1, PARTY_SLOTS do
  265. if Party[slot] ~= nil then Characters[Party[slot]].enabled = true end
  266. end
  267. self:refresh_reserve()
  268. self:set_held(nil)
  269. self:populate()
  270. self.cursor:set(1)
  271. ui_manager:get_widget("PhsMenu"):set_visible(true)
  272. return 0
  273. end,
  274. --- Closes the PHS menu and goes back to the main menu.
  275. hide = function(self)
  276. self:compact_party()
  277. ui_manager:get_widget("PhsMenu"):set_visible(false)
  278. self:deactivate()
  279. return 0
  280. end,
  281. }