battle_fx.lua 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. if UiContainer == nil then UiContainer = {} end
  2. --- Battle visual effects.
  3. --
  4. -- Simple generic effects: bursts of coloured particles by element, screen flashes and floating
  5. -- numbers. Everything is made with the sprites and text areas of the BattleFx screen, moved from
  6. -- here every frame. Positions of the scene are projected to the UI with camera_manager.
  7. UiContainer.BattleFx = {
  8. --- Size of the pools of the screen. Must match battle_fx.xml.
  9. PARTICLES = 60,
  10. NUMBERS = 8,
  11. --- Height, in scene units, above the position of a combatant where effects are shown.
  12. BODY_HEIGHT = 1.6,
  13. --- Effects of each element: colour, particle count, speed (UI units per second), gravity (UI
  14. -- units per second squared, negative makes them rise), size, life (seconds) and the flash
  15. -- (colour and strength) of the spells of the element. "converge" makes the particles come to
  16. -- the center instead of leaving it.
  17. ELEMENTS = {
  18. [0] = {colour = {1, 0.55, 0.1}, count = 24, speed = 90, gravity = -140, size = 10, life = 0.9, flash = {1, 0.5, 0.1, 0.3}},
  19. [1] = {colour = {0.6, 0.9, 1}, count = 18, speed = 110, gravity = 180, size = 8, life = 0.8, flash = {0.6, 0.9, 1, 0.3}},
  20. [2] = {colour = {1, 1, 0.4}, count = 16, speed = 260, gravity = 0, size = 6, life = 0.35, flash = {1, 1, 0.6, 0.55}},
  21. [3] = {colour = {0.65, 0.45, 0.25}, count = 18, speed = 140, gravity = 320, size = 9, life = 0.9, up = true},
  22. [4] = {colour = {0.5, 0.9, 0.3}, count = 16, speed = 50, gravity = -70, size = 9, life = 1.1, flash = {0.5, 0.9, 0.3, 0.2}},
  23. [5] = {colour = {0.6, 0.3, 0.9}, count = 20, speed = 120, gravity = 0, size = 8, life = 0.8, converge = true, flash = {0.3, 0.1, 0.5, 0.35}},
  24. [6] = {colour = {0.3, 0.6, 1}, count = 20, speed = 90, gravity = 240, size = 8, life = 0.9, flash = {0.3, 0.6, 1, 0.25}},
  25. [7] = {colour = {0.8, 1, 0.9}, count = 16, speed = 240, gravity = 0, size = 5, life = 0.5, flash = {0.8, 1, 0.9, 0.2}},
  26. [8] = {colour = {1, 1, 0.85}, count = 22, speed = 70, gravity = -100, size = 9, life = 1.0, flash = {1, 1, 0.9, 0.45}},
  27. },
  28. --- Effect of healing.
  29. HEAL = {colour = {0.4, 1, 0.55}, count = 16, speed = 45, gravity = -80, size = 9, life = 1.0, flash = {0.4, 1, 0.55, 0.2}},
  30. --- Effect of attacks without an element.
  31. PHYSICAL = {colour = {1, 1, 1}, count = 9, speed = 150, gravity = 260, size = 6, life = 0.35},
  32. --- Colours of the floating numbers.
  33. COLOUR_DAMAGE = {1, 1, 1},
  34. COLOUR_HEAL = {0.4, 1, 0.5},
  35. COLOUR_MP = {0.5, 0.7, 1},
  36. COLOUR_MISS = {0.7, 0.7, 0.7},
  37. --- Active particles and numbers.
  38. particles = {},
  39. numbers = {},
  40. --- Active screen flash.
  41. flash_state = nil,
  42. --- Game time of the last update.
  43. last_time = 0,
  44. --- Shows the effects screen and clears everything.
  45. show = function(self)
  46. self:clear()
  47. self.last_time = timer:get_game_time_total()
  48. ui_manager:get_widget("BattleFx"):set_visible(true)
  49. end,
  50. --- Hides the effects screen.
  51. hide = function(self)
  52. self:clear()
  53. ui_manager:get_widget("BattleFx"):set_visible(false)
  54. end,
  55. --- Removes every effect.
  56. clear = function(self)
  57. for _, particle in pairs(self.particles) do ui_manager:get_widget("BattleFx.P" .. tostring(particle.index)):set_visible(false) end
  58. for _, number in pairs(self.numbers) do ui_manager:get_widget("BattleFx.N" .. tostring(number.index)):set_visible(false) end
  59. ui_manager:get_widget("BattleFx.Flash"):set_visible(false)
  60. self.particles = {}
  61. self.numbers = {}
  62. self.flash_state = nil
  63. end,
  64. --- Gets the position of a combatant in the UI.
  65. --
  66. -- @param side Battle.TargetGroup.PARTY or ENEMIES.
  67. -- @param index Index of the combatant.
  68. -- @param height Height above the combatant, in scene units.
  69. -- @return X and Y in UI units, nil if the combatant has no entity.
  70. get_position = function(self, side, index, height)
  71. local entity = UiContainer.BattleUi:get_entity(side, index)
  72. if entity == nil then return nil end
  73. local x, y, z = entity:get_position()
  74. local ux, uy = camera_manager:project_to_ui(x, y, z + height)
  75. return ux, uy
  76. end,
  77. --- Finds a free particle of the pool.
  78. free_particle = function(self)
  79. for i = 1, self.PARTICLES do
  80. if self.particles[i] == nil then return i end
  81. end
  82. return nil
  83. end,
  84. --- Creates a particle.
  85. --
  86. -- @param p Table with x, y, vx, vy (per second), gravity, life, size, colour and alpha.
  87. spawn = function(self, p)
  88. local i = self:free_particle()
  89. if i == nil then do return end end
  90. p.index = i
  91. p.age = 0
  92. self.particles[i] = p
  93. local widget = ui_manager:get_widget("BattleFx.P" .. tostring(i))
  94. widget:set_colour(p.colour[1], p.colour[2], p.colour[3])
  95. widget:set_alpha(p.alpha or 1)
  96. widget:set_width(0, p.size)
  97. widget:set_height(0, p.size)
  98. widget:set_x(0, p.x - p.size / 2)
  99. widget:set_y(0, p.y - p.size / 2)
  100. widget:set_visible(true)
  101. end,
  102. --- Creates a burst of particles from a preset around a position.
  103. burst = function(self, preset, x, y)
  104. for _ = 1, preset.count do
  105. local angle = math.random() * 2 * math.pi
  106. local speed = preset.speed * (0.4 + 0.6 * math.random())
  107. local vx, vy = math.cos(angle) * speed, math.sin(angle) * speed
  108. local px, py = x, y
  109. if preset.converge then
  110. -- Start on a ring and move to the center.
  111. px, py = x - vx * 0.5, y - vy * 0.5
  112. vx, vy = vx, vy
  113. end
  114. if preset.up then vy = -math.abs(vy) * 1.3 end
  115. -- Slightly different tones.
  116. local shade = 0.8 + 0.2 * math.random()
  117. self:spawn({
  118. x = px, y = py, vx = vx, vy = vy, gravity = preset.gravity,
  119. life = preset.life * (0.7 + 0.5 * math.random()), size = preset.size * (0.6 + 0.6 * math.random()),
  120. colour = {preset.colour[1] * shade, preset.colour[2] * shade, preset.colour[3] * shade},
  121. })
  122. end
  123. end,
  124. --- Shows a screen flash that fades out.
  125. --
  126. -- @param colour Table with r, g, b.
  127. -- @param strength Starting alpha.
  128. -- @param life Seconds it lasts.
  129. flash = function(self, colour, strength, life)
  130. self.flash_state = {colour = colour, strength = strength, life = life or 0.35, age = 0}
  131. local widget = ui_manager:get_widget("BattleFx.Flash")
  132. widget:set_colour(colour[1], colour[2], colour[3])
  133. widget:set_alpha(strength)
  134. widget:set_visible(true)
  135. end,
  136. --- Gets the effect preset of an attack, for the element it has or if it heals.
  137. get_preset = function(self, attack, heals)
  138. if heals then return self.HEAL end
  139. if attack ~= nil and attack.elements ~= nil and #attack.elements > 0 then
  140. return self.ELEMENTS[attack.elements[1]] or self.PHYSICAL
  141. end
  142. return self.PHYSICAL
  143. end,
  144. --- Effect of casting a spell: sparks around the caster.
  145. cast = function(self, attack, side, index)
  146. -- Only magic is cast. Other attacks just hit.
  147. if attack == nil or attack.id == nil or attack.id < 0 or (attack.damage.formula or 0) ~= 2 then do return end end
  148. local x, y = self:get_position(side, index, self.BODY_HEIGHT)
  149. if x == nil then do return end end
  150. local preset = self:get_preset(attack, (attack.damage.modifier or 0) == 5 and (attack.damage.formula or 0) == 2)
  151. for _ = 1, 8 do
  152. local angle = math.random() * 2 * math.pi
  153. self:spawn({
  154. x = x + math.cos(angle) * 30, y = y + math.sin(angle) * 30,
  155. vx = -math.cos(angle) * 40, vy = -math.sin(angle) * 40 - 30, gravity = 0,
  156. life = 0.8, size = 7, colour = preset.colour,
  157. })
  158. end
  159. end,
  160. --- Effect of an attack that hits a target: particles and, for spells, a screen flash.
  161. --
  162. -- @param attack Attack data.
  163. -- @param result The result of the attack, as in BattleUi.calculate_attack.
  164. hit = function(self, attack, side, index, result)
  165. local x, y = self:get_position(side, index, self.BODY_HEIGHT)
  166. if x == nil then do return end end
  167. local preset = self:get_preset(attack, result.amount < 0)
  168. self:burst(preset, x, y)
  169. if preset.flash ~= nil and attack.id ~= nil and attack.id >= 0 then
  170. self:flash({preset.flash[1], preset.flash[2], preset.flash[3]}, preset.flash[4], 0.4)
  171. end
  172. end,
  173. --- Shows a floating text over a combatant.
  174. --
  175. -- @param text The text to show.
  176. -- @param colour Table with r, g, b.
  177. text = function(self, side, index, text, colour)
  178. local x, y = self:get_position(side, index, self.BODY_HEIGHT + 0.6)
  179. if x == nil then do return end end
  180. local slot = nil
  181. for i = 1, self.NUMBERS do
  182. if self.numbers[i] == nil then slot = i break end
  183. end
  184. -- If all are used, take the one that has been there the longest.
  185. if slot == nil then
  186. local oldest = 1
  187. for i = 2, self.NUMBERS do
  188. if self.numbers[i].age > self.numbers[oldest].age then oldest = i end
  189. end
  190. slot = oldest
  191. end
  192. -- Spread the numbers of the same target a bit so they don't pile up.
  193. self.numbers[slot] = {
  194. index = slot, x = x + (math.random() - 0.5) * 40 - 7 * #text / 2, y = y - 10 * math.random(),
  195. age = 0, life = 1.1, text = text, colour = colour,
  196. }
  197. local number = self.numbers[slot]
  198. local widget = ui_manager:get_widget("BattleFx.N" .. tostring(slot))
  199. -- The colour is applied when the text is set.
  200. widget:set_colour(colour[1], colour[2], colour[3])
  201. widget:set_alpha(1)
  202. widget:set_text(text)
  203. widget:set_x(0, number.x)
  204. widget:set_y(0, number.y)
  205. widget:set_visible(true)
  206. end,
  207. --- Shows the HP or MP lost or recovered by a combatant.
  208. --
  209. -- @param amount Positive for damage, negative for recovery.
  210. -- @param mp True if it's MP.
  211. number = function(self, side, index, amount, mp)
  212. local colour = self.COLOUR_DAMAGE
  213. if amount < 0 then colour = self.COLOUR_HEAL end
  214. if mp then colour = self.COLOUR_MP end
  215. self:text(side, index, tostring(math.abs(amount)), colour)
  216. end,
  217. --- Advances the effects.
  218. --
  219. -- @param now Game time.
  220. update = function(self, now)
  221. local dt = math.max(0, math.min(0.1, now - self.last_time))
  222. self.last_time = now
  223. for i, p in pairs(self.particles) do
  224. p.age = p.age + dt
  225. local widget = ui_manager:get_widget("BattleFx.P" .. tostring(i))
  226. if p.age >= p.life then
  227. widget:set_visible(false)
  228. self.particles[i] = nil
  229. else
  230. p.x = p.x + p.vx * dt
  231. p.y = p.y + p.vy * dt
  232. p.vy = p.vy + p.gravity * dt
  233. local fade = 1 - p.age / p.life
  234. widget:set_alpha((p.alpha or 1) * fade)
  235. widget:set_x(0, p.x - p.size / 2)
  236. widget:set_y(0, p.y - p.size / 2)
  237. end
  238. end
  239. for i, n in pairs(self.numbers) do
  240. n.age = n.age + dt
  241. local widget = ui_manager:get_widget("BattleFx.N" .. tostring(i))
  242. if n.age >= n.life then
  243. widget:set_visible(false)
  244. self.numbers[i] = nil
  245. else
  246. -- Rise, and fade out in the last part. Text colour is baked when it's set, so the
  247. -- text is set again with the new alpha.
  248. local alpha = math.min(1, (n.life - n.age) / (n.life * 0.4))
  249. widget:set_colour(n.colour[1], n.colour[2], n.colour[3])
  250. widget:set_alpha(alpha)
  251. widget:set_text(n.text)
  252. widget:set_x(0, n.x)
  253. widget:set_y(0, n.y - 45 * (n.age / n.life))
  254. end
  255. end
  256. local f = self.flash_state
  257. if f ~= nil then
  258. f.age = f.age + dt
  259. local widget = ui_manager:get_widget("BattleFx.Flash")
  260. if f.age >= f.life then
  261. widget:set_visible(false)
  262. self.flash_state = nil
  263. else
  264. widget:set_alpha(f.strength * (1 - f.age / f.life))
  265. end
  266. end
  267. end,
  268. }