init.lua 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. --[[
  2. MIT License
  3. Copyright (c) 2017 SSYGEN
  4. Permission is hereby granted, free of charge, to any person obtaining a copy
  5. of this software and associated documentation files (the "Software"), to deal
  6. in the Software without restriction, including without limitation the rights
  7. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. copies of the Software, and to permit persons to whom the Software is
  9. furnished to do so, subject to the following conditions:
  10. The above copyright notice and this permission notice shall be included in all
  11. copies or substantial portions of the Software.
  12. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  13. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  14. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  15. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  16. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  17. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  18. SOFTWARE.
  19. ]]--
  20. local function lerp(a, b, x) return a + (b - a)*x end
  21. local function csnap(v, x) return math.ceil(v/x)*x - x/2 end
  22. -- Shake according to https://jonny.morrill.me/en/blog/gamedev-how-to-implement-a-camera-shake-effect/
  23. local function newShake(amplitude, duration, frequency)
  24. local self = {
  25. amplitude = amplitude or 0,
  26. duration = duration or 0,
  27. frequency = frequency or 60,
  28. samples = {},
  29. start_time = love.timer.getTime()*1000,
  30. t = 0,
  31. shaking = true,
  32. }
  33. local sample_count = (self.duration/1000)*self.frequency
  34. for i = 1, sample_count do self.samples[i] = 2*love.math.random()-1 end
  35. return self
  36. end
  37. local function updateShake(self, dt)
  38. self.t = love.timer.getTime()*1000 - self.start_time
  39. if self.t > self.duration then self.shaking = false end
  40. end
  41. local function shakeNoise(self, s)
  42. if s >= #self.samples then return 0 end
  43. return self.samples[s] or 0
  44. end
  45. local function shakeDecay(self, t)
  46. if t > self.duration then return 0 end
  47. return (self.duration - t)/self.duration
  48. end
  49. local function getShakeAmplitude(self, t)
  50. if not t then
  51. if not self.shaking then return 0 end
  52. t = self.t
  53. end
  54. local s = (t/1000)*self.frequency
  55. local s0 = math.floor(s)
  56. local s1 = s0 + 1
  57. local k = shakeDecay(self, t)
  58. return self.amplitude*(shakeNoise(self, s0) + (s - s0)*(shakeNoise(self, s1) - shakeNoise(self, s0)))*k
  59. end
  60. -- Camera
  61. local Camera = {}
  62. Camera.__index = Camera
  63. local function new(x, y, w, h, scale, rotation)
  64. return setmetatable({
  65. x = x or (w or love.graphics.getWidth())/2, y = y or (h or love.graphics.getHeight())/2,
  66. mx = x or (w or love.graphics.getWidth())/2, my = y or (h or love.graphics.getHeight())/2,
  67. screen_x = x or (w or love.graphics.getWidth())/2, screen_y = y or (h or love.graphics.getHeight())/2,
  68. w = w or love.graphics.getWidth(), h = h or love.graphics.getHeight(),
  69. scale = scale or 1,
  70. rotation = rotation or 0,
  71. horizontal_shakes = {}, vertical_shakes = {},
  72. target_x = nil, target_y = nil,
  73. scroll_x = 0, scroll_y = 0,
  74. last_target_x = nil, last_target_y = nil,
  75. follow_lerp_x = 1, follow_lerp_y = 1,
  76. follow_lead_x = 0, follow_lead_y = 0,
  77. deadzone = nil, bound = nil,
  78. draw_deadzone = false,
  79. flash_duration = 1, flash_timer = 0, flash_color = {0, 0, 0, 1},
  80. last_horizontal_shake_amount = 0, last_vertical_shake_amount = 0,
  81. fade_duration = 1, fade_timer = 0, fade_color = {0, 0, 0, 0},
  82. }, Camera)
  83. end
  84. function Camera:attach()
  85. love.graphics.push()
  86. love.graphics.translate(self.w/2, self.h/2)
  87. love.graphics.scale(self.scale)
  88. love.graphics.rotate(self.rotation)
  89. love.graphics.translate(-self.x, -self.y)
  90. end
  91. function Camera:detach()
  92. love.graphics.pop()
  93. end
  94. function Camera:move(dx, dy)
  95. self.x, self.y = self.x + dx, self.y + dy
  96. end
  97. function Camera:toWorldCoords(x, y)
  98. local c, s = math.cos(self.rotation), math.sin(self.rotation)
  99. x, y = (x - self.w/2)/self.scale, (y - self.h/2)/self.scale
  100. x, y = c*x - s*y, s*x + c*y
  101. return x + self.x, y + self.y
  102. end
  103. function Camera:toCameraCoords(x, y)
  104. local c, s = math.cos(self.rotation), math.sin(self.rotation)
  105. x, y = x - self.x, y - self.y
  106. x, y = c*x - s*y, s*x + c*y
  107. return x*self.scale + self.w/2, y*self.scale + self.h/2
  108. end
  109. function Camera:getMousePosition()
  110. return self:toWorldCoords(love.mouse.getPosition())
  111. end
  112. function Camera:shake(intensity, duration, frequency, axes)
  113. if not axes then axes = 'XY' end
  114. axes = string.upper(axes)
  115. if string.find(axes, 'X') then table.insert(self.horizontal_shakes, newShake(intensity, duration*1000, frequency)) end
  116. if string.find(axes, 'Y') then table.insert(self.vertical_shakes, newShake(intensity, duration*1000, frequency)) end
  117. end
  118. function Camera:update(dt)
  119. self.mx, self.my = self:toWorldCoords(love.mouse.getPosition())
  120. -- Flash --
  121. if self.flashing then
  122. self.flash_timer = self.flash_timer + dt
  123. if self.flash_timer > self.flash_duration then
  124. self.flash_timer = 0
  125. self.flashing = false
  126. end
  127. end
  128. -- Fade --
  129. if self.fading then
  130. self.fade_timer = self.fade_timer + dt
  131. self.fade_color = {
  132. lerp(self.base_fade_color[1], self.target_fade_color[1], self.fade_timer/self.fade_duration),
  133. lerp(self.base_fade_color[2], self.target_fade_color[2], self.fade_timer/self.fade_duration),
  134. lerp(self.base_fade_color[3], self.target_fade_color[3], self.fade_timer/self.fade_duration),
  135. lerp(self.base_fade_color[4], self.target_fade_color[4], self.fade_timer/self.fade_duration),
  136. }
  137. if self.fade_timer > self.fade_duration then
  138. self.fade_timer = 0
  139. self.fading = false
  140. if self.fade_action then self.fade_action() end
  141. end
  142. end
  143. -- Shake --
  144. local horizontal_shake_amount, vertical_shake_amount = 0, 0
  145. for i = #self.horizontal_shakes, 1, -1 do
  146. updateShake(self.horizontal_shakes[i], dt)
  147. horizontal_shake_amount = horizontal_shake_amount + getShakeAmplitude(self.horizontal_shakes[i])
  148. if not self.horizontal_shakes[i].shaking then table.remove(self.horizontal_shakes, i) end
  149. end
  150. for i = #self.vertical_shakes, 1, -1 do
  151. updateShake(self.vertical_shakes[i], dt)
  152. vertical_shake_amount = vertical_shake_amount + getShakeAmplitude(self.vertical_shakes[i])
  153. if not self.vertical_shakes[i].shaking then table.remove(self.vertical_shakes, i) end
  154. end
  155. self.x, self.y = self.x - self.last_horizontal_shake_amount, self.y - self.last_vertical_shake_amount
  156. self:move(horizontal_shake_amount, vertical_shake_amount)
  157. self.last_horizontal_shake_amount, self.last_vertical_shake_amount = horizontal_shake_amount, vertical_shake_amount
  158. -- Follow --
  159. if not self.target_x and not self.target_y then return end
  160. -- Set follow style deadzones
  161. if self.follow_style == 'LOCKON' then
  162. local w, h = self.w/16, self.w/16
  163. self:setDeadzone((self.w - w)/2, (self.h - h)/2, w, h)
  164. elseif self.follow_style == 'PLATFORMER' then
  165. local w, h = self.w/8, self.h/3
  166. self:setDeadzone((self.w - w)/2, (self.h - h)/2 - h*0.25, w, h)
  167. elseif self.follow_style == 'TOPDOWN' then
  168. local s = math.max(self.w, self.h)/4
  169. self:setDeadzone((self.w - s)/2, (self.h - s)/2, s, s)
  170. elseif self.follow_style == 'TOPDOWN_TIGHT' then
  171. local s = math.max(self.w, self.h)/8
  172. self:setDeadzone((self.w - s)/2, (self.h - s)/2, s, s)
  173. elseif self.follow_style == 'SCREEN_BY_SCREEN' then
  174. self:setDeadzone(0, 0, 0, 0)
  175. elseif self.follow_style == 'NO_DEADZONE' then
  176. self.deadzone = nil
  177. end
  178. -- No deadzone means we just track the target with no lerp
  179. if not self.deadzone then
  180. self.x, self.y = self.target_x, self.target_y
  181. if self.bound then
  182. self.x = math.min(math.max(self.x, self.bounds_min_x + self.w/2), self.bounds_max_x - self.w/2)
  183. self.y = math.min(math.max(self.y, self.bounds_min_y + self.h/2), self.bounds_max_y - self.h/2)
  184. end
  185. return
  186. end
  187. -- Convert appropriate variables to camera coordinates since the deadzone is applied in terms of the camera and not the world
  188. local dx1, dy1, dx2, dy2 = self.deadzone_x, self.deadzone_y, self.deadzone_x + self.deadzone_w, self.deadzone_y + self.deadzone_h
  189. local scroll_x, scroll_y = 0, 0
  190. local target_x, target_y = self:toCameraCoords(self.target_x, self.target_y)
  191. local x, y = self:toCameraCoords(self.x, self.y)
  192. -- Screen by screen follow mode needs to be handled a bit differently
  193. if self.follow_style == 'SCREEN_BY_SCREEN' then
  194. -- Don't change self.screen_x/y if already at the boundaries
  195. if self.bound then
  196. if self.x > self.bounds_min_x + self.w/2 and target_x < 0 then self.screen_x = csnap(self.screen_x - self.w/self.scale, self.w/self.scale) end
  197. if self.x < self.bounds_max_x - self.w/2 and target_x >= self.w then self.screen_x = csnap(self.screen_x + self.w/self.scale, self.w/self.scale) end
  198. if self.y > self.bounds_min_y + self.h/2 and target_y < 0 then self.screen_y = csnap(self.screen_y - self.h/self.scale, self.h/self.scale) end
  199. if self.y < self.bounds_max_y - self.h/2 and target_y >= self.h then self.screen_y = csnap(self.screen_y + self.h/self.scale, self.h/self.scale) end
  200. -- Move to the next screen if the target is outside the screen boundaries
  201. else
  202. if target_x < 0 then self.screen_x = csnap(self.screen_x - self.w/self.scale, self.w/self.scale) end
  203. if target_x >= self.w then self.screen_x = csnap(self.screen_x + self.w/self.scale, self.w/self.scale) end
  204. if target_y < 0 then self.screen_y = csnap(self.screen_y - self.h/self.scale, self.h/self.scale) end
  205. if target_y >= self.h then self.screen_y = csnap(self.screen_y + self.h/self.scale, self.h/self.scale) end
  206. end
  207. self.x = lerp(self.x, self.screen_x, self.follow_lerp_x)
  208. self.y = lerp(self.y, self.screen_y, self.follow_lerp_y)
  209. -- Apply bounds
  210. if self.bound then
  211. self.x = math.min(math.max(self.x, self.bounds_min_x + self.w/2), self.bounds_max_x - self.w/2)
  212. self.y = math.min(math.max(self.y, self.bounds_min_y + self.h/2), self.bounds_max_y - self.h/2)
  213. end
  214. -- All other follow modes
  215. else
  216. -- Figure out how much the camera needs to scroll
  217. if target_x < x + (dx1 + dx2 - x) then
  218. local d = target_x - dx1
  219. if d < 0 then scroll_x = d end
  220. end
  221. if target_x > x - (dx1 + dx2 - x) then
  222. local d = target_x - dx2
  223. if d > 0 then scroll_x = d end
  224. end
  225. if target_y < y + (dy1 + dy2 - y) then
  226. local d = target_y - dy1
  227. if d < 0 then scroll_y = d end
  228. end
  229. if target_y > y - (dy1 + dy2 - y) then
  230. local d = target_y - dy2
  231. if d > 0 then scroll_y = d end
  232. end
  233. -- Apply lead
  234. if not self.last_target_x and not self.last_target_y then self.last_target_x, self.last_target_y = self.target_x, self.target_y end
  235. scroll_x = scroll_x + (self.target_x - self.last_target_x)*self.follow_lead_x
  236. scroll_y = scroll_y + (self.target_y - self.last_target_y)*self.follow_lead_y
  237. self.last_target_x, self.last_target_y = self.target_x, self.target_y
  238. -- Scroll towards target with lerp
  239. self.x = lerp(self.x, self.x + scroll_x, self.follow_lerp_x)
  240. self.y = lerp(self.y, self.y + scroll_y, self.follow_lerp_y)
  241. -- Apply bounds
  242. if self.bound then
  243. self.x = math.min(math.max(self.x, self.bounds_min_x + self.w/2), self.bounds_max_x - self.w/2)
  244. self.y = math.min(math.max(self.y, self.bounds_min_y + self.h/2), self.bounds_max_y - self.h/2)
  245. end
  246. end
  247. end
  248. function Camera:draw()
  249. if self.draw_deadzone and self.deadzone then
  250. local n = love.graphics.getLineWidth()
  251. love.graphics.setLineWidth(2)
  252. love.graphics.line(self.deadzone_x - 1, self.deadzone_y, self.deadzone_x + 6, self.deadzone_y)
  253. love.graphics.line(self.deadzone_x, self.deadzone_y, self.deadzone_x, self.deadzone_y + 6)
  254. love.graphics.line(self.deadzone_x - 1, self.deadzone_y + self.deadzone_h, self.deadzone_x + 6, self.deadzone_y + self.deadzone_h)
  255. love.graphics.line(self.deadzone_x, self.deadzone_y + self.deadzone_h, self.deadzone_x, self.deadzone_y + self.deadzone_h - 6)
  256. love.graphics.line(self.deadzone_x + self.deadzone_w + 1, self.deadzone_y + self.deadzone_h, self.deadzone_x + self.deadzone_w - 6, self.deadzone_y + self.deadzone_h)
  257. love.graphics.line(self.deadzone_x + self.deadzone_w, self.deadzone_y + self.deadzone_h, self.deadzone_x + self.deadzone_w, self.deadzone_y + self.deadzone_h - 6)
  258. love.graphics.line(self.deadzone_x + self.deadzone_w + 1, self.deadzone_y, self.deadzone_x + self.deadzone_w - 6, self.deadzone_y)
  259. love.graphics.line(self.deadzone_x + self.deadzone_w, self.deadzone_y, self.deadzone_x + self.deadzone_w, self.deadzone_y + 6)
  260. love.graphics.setLineWidth(n)
  261. end
  262. if self.flashing then
  263. local r, g, b, a = love.graphics.getColor()
  264. love.graphics.setColor(self.flash_color)
  265. love.graphics.rectangle('fill', 0, 0, self.w, self.h)
  266. love.graphics.setColor(r, g, b, a)
  267. end
  268. local r, g, b, a = love.graphics.getColor()
  269. love.graphics.setColor(self.fade_color)
  270. love.graphics.rectangle('fill', 0, 0, self.w, self.h)
  271. love.graphics.setColor(r, g, b, a)
  272. end
  273. function Camera:follow(x, y)
  274. self.target_x, self.target_y = x, y
  275. end
  276. function Camera:setDeadzone(x, y, w, h)
  277. self.deadzone = true
  278. self.deadzone_x = x
  279. self.deadzone_y = y
  280. self.deadzone_w = w
  281. self.deadzone_h = h
  282. end
  283. function Camera:setBounds(x, y, w, h)
  284. self.bound = true
  285. self.bounds_min_x = x
  286. self.bounds_min_y = y
  287. self.bounds_max_x = x + w
  288. self.bounds_max_y = y + h
  289. end
  290. function Camera:setFollowStyle(follow_style)
  291. self.follow_style = follow_style
  292. end
  293. function Camera:setFollowLerp(x, y)
  294. self.follow_lerp_x = x
  295. self.follow_lerp_y = y or x
  296. end
  297. function Camera:setFollowLead(x, y)
  298. self.follow_lead_x = x
  299. self.follow_lead_y = y or x
  300. end
  301. function Camera:flash(duration, color)
  302. self.flash_duration = duration
  303. self.flash_color = color or self.flash_color
  304. self.flash_timer = 0
  305. self.flashing = true
  306. end
  307. function Camera:fade(duration, color, action)
  308. self.fade_duration = duration
  309. self.base_fade_color = self.fade_color
  310. self.target_fade_color = color
  311. self.fade_timer = 0
  312. self.fade_action = action
  313. self.fading = true
  314. end
  315. return setmetatable({new = new}, {__call = function(_, ...) return new(...) end})