main.lua 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. local Board = require 'board'
  2. local led = require 'led'
  3. local shader = require 'shader'
  4. local DEBUG_backBumper = false -- Set this to "true" and the ball will bounce back instead of dying
  5. -- Oculus Go uses a fixed camera position, so we have to change where things are drawn
  6. local fixedCamera = lovr.headset.getName() == "Oculus Go"
  7. local tim = 0 -- Accumulated time
  8. -- -- -- Constants -- -- --
  9. -- Board constants
  10. local bWidth = 8 -- Cell-size width and height of board
  11. local bHeight = 5
  12. local bDistanceAway = 6 -- Distance from paddle to blocks
  13. local bBackBuffer = 1 -- Space between blocks and back wall
  14. local bKill = -3 -- Distance from paddle to kill plane
  15. -- Constants for drawing cubes
  16. local uWidth = 0.25 -- One "unit"-- one cell in the board
  17. local uHeight = 0.25
  18. local cubeWidth = uWidth - 0.025 -- A cube should be the size of a board cell minus a margin
  19. local cubeHeight = uHeight - 0.025
  20. local gRight = lovr.math.newVec3(1, 0, 0) -- The vector basis for coordinates on the board
  21. local gDown = lovr.math.newVec3(0, 0, -1)
  22. local gCloser = lovr.math.newVec3(0, 1, 0)
  23. local uRight = lovr.math.newVec3( gRight*uWidth ) -- Vector basis for coordinates on the board, in units
  24. local uDown = lovr.math.newVec3( gDown*uHeight )
  25. -- The upper left corner of the board:
  26. local bUlCorner = lovr.math.newVec3( uDown * (bDistanceAway + bHeight) + gRight * -(uWidth * bWidth/2) )
  27. local function bCenter(x,y) -- The center of a specific cell on the board, for drawing
  28. return bUlCorner + uRight * ( (x-1) + 0.5) + uDown * ( -(y-1) - 0.5)
  29. end
  30. local function bCornerGrid(x, y) -- The corner of a specific cell on the board, for math
  31. return x - bWidth/2, bDistanceAway + bHeight - y
  32. end
  33. local function bCornerGridReverse(x,y) -- Given a world position, what cell is it?
  34. x = x + bWidth/2
  35. y = bDistanceAway + bHeight - y
  36. return math.floor(x) + 1, math.floor(y) + 1
  37. end
  38. -- Constants for paddle
  39. local pWidth = 2
  40. local pHeight = 1/3
  41. local pSize = lovr.math.newVec3( (gRight*pWidth + gDown*pHeight + gCloser)*cubeHeight )
  42. local function pGrid(x) -- Get paddle center position in world coordinates, given a 0..1 placement
  43. return -bWidth/2 + bWidth * x
  44. end
  45. -- Constants for "LED" score display
  46. local lCube = uWidth -- Currently LED and board cubes are the same size
  47. -- Vector basis for score display screen:
  48. local lUlRoot = lovr.math.newVec3( bUlCorner + (gDown * 4 + gCloser * 2 + gRight) * uHeight + uRight * (bWidth/2) )
  49. local lRight = lovr.math.newVec3( gRight * lCube )
  50. local lDown = lovr.math.newVec3( gCloser * -lCube )
  51. -- Ball constants
  52. local ballWidth = 0.25
  53. local ballRoot = lovr.math.newVec3( gRight*(ballWidth*uWidth/2) )
  54. -- "Scripted sequence" constants
  55. local gameStateDeathRollover = {silence=1.5, tone=3, reset = 3 + 1.753469387755102}
  56. local soundsWinStrum = {1,3,6,2}
  57. local soundsWinStrumSpeed = 8164/44100
  58. -- -- -- State -- -- --
  59. local board = nil -- Current board
  60. local gameState -- One of: {}, {"dead", start=[death time], substate=[substate]}, or {"win", start=[win time]}
  61. local gameLevel -- Current level # (affects speed)
  62. local points -- Current points scored
  63. local remaining -- Number of blocks remaining on current board
  64. local controllerModel -- Current loaded controller model, if any
  65. local sounds -- Audio objects (to be loaded)
  66. local function newVec2(x, y) return lovr.math.newVec3(x, y, 0) end -- For 2D vecs we'll just ignore z
  67. local function vec2(x, y) return lovr.math.vec3(x, y, 0) end -- For 2D vecs we'll just ignore z
  68. local ballAt = newVec2(0, 1) -- Current ball position
  69. local ballVel = newVec2() -- Current ball velocity
  70. -- -- -- Game code -- -- --
  71. function lovr.conf(t)
  72. t.identity = 'Break it'
  73. t.window.title = t.identity
  74. end
  75. function gameReset() -- Reset state completely, as if after a death
  76. gameState = {}
  77. gameLevel = 1
  78. points = 0
  79. ballVel:set(vec2(0.0625, 0.0625))
  80. end
  81. function boardReset() -- Reset board contents, as for death or new-level start
  82. board = Board.fill({}, bWidth, bHeight, true)
  83. remaining = bWidth*bHeight
  84. end
  85. function lovr.load()
  86. lovr.graphics.setBackgroundColor(.1, .1, .1)
  87. lovr.headset.setClipDistance(0.1, 3000)
  88. gameReset()
  89. boardReset()
  90. sounds = {}
  91. if lovr.audio then
  92. for i=0,5 do
  93. table.insert(sounds, lovr.audio.newSource(string.format("break-bwomp-song-1-split-%d.ogg", i), 'static'))
  94. sounds.fail = lovr.audio.newSource("break-buzzer.ogg", 'static')
  95. sounds.restart = lovr.audio.newSource("break-countdown.ogg", 'static')
  96. end
  97. end
  98. end
  99. local function cube(v) -- Draw board block
  100. local x,y,z = v:unpack()
  101. lovr.graphics.cube('fill', x,y,z, cubeWidth)
  102. end
  103. local function ledCube(v) -- Draw score display block
  104. local x,y,z = v:unpack()
  105. lovr.graphics.cube('fill', x,y,z, lCube)
  106. end
  107. local function paddle(x) -- Draw paddle. Expect 0..1
  108. local x,y,z = (gRight*((-0.5 + x)*bWidth*uWidth)):unpack()
  109. local xd, yd, zd = pSize:unpack()
  110. lovr.graphics.box('fill', x, y, z, xd, yd, zd)
  111. end
  112. local function ballXyz(bv) -- Get the XYZ position of the ball (for drawing or sound)
  113. local bx, by = bv:unpack()
  114. return ballRoot + uRight*bx + uDown*by -- Temporary
  115. end
  116. local function ball(bv) -- Draw the ball
  117. local x,y,z = ballXyz(bv):unpack()
  118. lovr.graphics.cube('fill', x, y, z, cubeWidth*ballWidth)
  119. end
  120. local function tie(x) -- tie fighter operator <=>
  121. if x > 0 then return 1
  122. elseif x < 0 then return -1
  123. else return 0
  124. end
  125. end
  126. -- Display state
  127. local paddleAt = 0.5
  128. local screen = {}
  129. -- The sounds table has a list of "notes" that play in rotating fashion when the ball bounces off something.
  130. local lastSound
  131. local pendingSound = 1
  132. local function nextSound(at, forceSound) -- Play a sound at a position. If forceSound is nil, assume the next "note"
  133. if lastSound then lastSound:stop() lastSound:rewind() end -- Don't let sounds overlap
  134. lastSound = forceSound or sounds[pendingSound]
  135. pendingSound = pendingSound + 1
  136. if pendingSound > #sounds then pendingSound = 1 end
  137. if lastSound then
  138. lastSound:setPosition(at.x, at.y, at.z)
  139. lastSound:play()
  140. end
  141. end
  142. local function score() -- Block consumed, increment score
  143. points = points + 1
  144. remaining = remaining - 1
  145. if remaining == 0 then -- Beat the level
  146. gameState = {"win", start=tim, strumAt=0}
  147. end
  148. end
  149. -- "Cheat" handling
  150. -- At the end of the game, the player can get stuck in a state where they are bouncing eternally,
  151. -- hoping to hit a block but always missing. This is boring, so if the player does a roundtrip
  152. -- paddle->back->paddle->back without hitting a block, just delete a block at random.
  153. local function cheatBackWall() -- Back hit
  154. if gameState.cheat and gameState.cheat.x then -- A block is picked out to delete (Second clause might be unnecessary)
  155. Board.set(board, gameState.cheat.x, gameState.cheat.y, false)
  156. gameState.cheat.x = false
  157. score()
  158. end
  159. end
  160. local function cheatPaddle() -- Paddle hit
  161. -- If the cheat is not disarmed (ie: we've hit the back and come back without hitting a block in between),
  162. -- pick out the block to delete now so it can blink
  163. if gameState.cheat and remaining > 0 then -- Second clause might be unnecessary
  164. local cheatSelect = lovr.math.random(remaining) -- We will count up the blocks until we reach this number, then stop
  165. for x=1,bWidth do -- Iterate over the board space
  166. if gameState.cheat.x then break end -- Already found a block to delete
  167. for y=1,bHeight do
  168. if Board.get(board,x,y) then
  169. cheatSelect = cheatSelect - 1
  170. if cheatSelect == 0 then -- Found our target block
  171. gameState.cheat.x, gameState.cheat.y, gameState.cheat.start = x,y,tim -- Save current time so blink can key off it
  172. break
  173. end
  174. end
  175. end
  176. end
  177. else
  178. -- Whenever we hit the paddle we "arm" the cheat.
  179. if not gameState.cheat then gameState.cheat = {} end -- The cheat state is stored in gameState so it gets cleared on death/level clear
  180. end
  181. end
  182. local function cheatDisarm() -- Whenever we hit a block we delete the table that constitutes "arming"
  183. gameState.cheat = nil
  184. end
  185. function lovr.update(dt)
  186. tim = tim + dt
  187. -- Use the highest-numbered hand, which is probably the right hand.
  188. local controllerNames = lovr.headset.getHands()
  189. local controller = controllerNames[#controllerNames]
  190. if #controllerNames then -- Turn roll of selected controller into a paddle position 0..1
  191. local q = lovr.math.quat( lovr.headset.getOrientation(controller) ):normalize()
  192. paddleAt = (q.z + 1)/2
  193. end
  194. -- If a death sequence has finished and the game should reset, handle that at the start of the frame.
  195. if gameState[1] == "dead" and tim - gameState.start > gameStateDeathRollover.reset then
  196. gameReset()
  197. boardReset()
  198. ballAt:set(vec2(-2 + tim%4, 1)) -- On reset randomize the ball position a little
  199. if ballAt.x < 0 then ballVel.x = -ballVel.x end -- Set starting velocity away from center
  200. end
  201. if gameState[1] ~= "dead" then -- Not dead. Normal game logic
  202. local ballMargin = ballWidth/2 -- A few last minute constants
  203. local borderWidth = bWidth/2
  204. local borderHeight = bHeight + bDistanceAway + bBackBuffer
  205. local paddleMargin = pHeight/2
  206. local startAbovePaddle = ballAt.y - ballMargin > paddleMargin -- At the start of the frame, was the ball above the paddle?
  207. -- The ball has crossed over a line it can't cross. Reverse its direction and apply its overshoot in the other direction
  208. local function bounce(key, dir, limit)
  209. local at = ballAt[key]
  210. local cVal = at + dir * ballMargin
  211. local cAgainst = limit
  212. ballVel[key] = ballVel[key] * -1
  213. ballAt[key] = at + (cAgainst - cVal) * 2
  214. -- Whenever the ball bounces off something, play a sound-- EXCEPT,
  215. -- In the time between the last block of a level clears and the ball next hits the paddle, we mute
  216. if gameState[1] ~= "win" then nextSound(ballXyz(ballAt)) end
  217. end
  218. -- Move up/down
  219. ballAt.y = ballAt.y + ballVel.y
  220. local cellX, cellY = bCornerGridReverse(ballAt.x, ballAt.y + tie(ballVel.y)*ballMargin)
  221. if Board.get(board, cellX, cellY) then -- Vertically collide with block
  222. Board.set(board, cellX, cellY, false)
  223. score()
  224. cheatDisarm()
  225. bounce('y', tie(ballVel.y), ({bCornerGrid(cellX, cellY)})[2] + (ballVel.y > 0 and 0 or 1))
  226. end
  227. -- Move left/right
  228. ballAt.x = ballAt.x + ballVel.x
  229. cellX, cellY = bCornerGridReverse(ballAt.x + tie(ballVel.x)*ballMargin, ballAt.y)
  230. if Board.get(board, cellX, cellY) then -- Horizontally collide with block
  231. Board.set(board, cellX, cellY, false)
  232. score()
  233. cheatDisarm()
  234. bounce('x', tie(ballVel.x), bCornerGrid(cellX, cellY) + (ballVel.x > 0 and -1 or 0))
  235. end
  236. -- Side barrier check
  237. if ballAt.x >= borderWidth - ballMargin then
  238. bounce('x', 1, borderWidth)
  239. end
  240. if ballAt.x <= -(borderWidth - ballMargin) then
  241. bounce('x', -1, -borderWidth)
  242. end
  243. -- Top barrier check
  244. if ballAt.y >= borderHeight - ballMargin then
  245. cheatBackWall()
  246. bounce('y', 1, borderHeight)
  247. end
  248. if gameState[1] == "win" then -- In the second after collecing the last block of a level, play four quick "victory" notes
  249. local shouldStrum = 1 + math.floor((tim - gameState.start)/soundsWinStrumSpeed)
  250. if shouldStrum > gameState.strumAt then
  251. gameState.strumAt = gameState.strumAt+1
  252. local whichStrum = soundsWinStrum[gameState.strumAt]
  253. if whichStrum then nextSound(ballXyz(ballAt), sounds[whichStrum]) end
  254. end
  255. end
  256. local endAbovePaddle = ballAt.y - ballMargin > paddleMargin -- Now that we're at the end of the frame, is the ball above the paddle?
  257. -- Did the ball move from below the paddle to above the paddle during this frame?
  258. if startAbovePaddle and not endAbovePaddle then
  259. if math.abs(pGrid(paddleAt) - ballAt.x) < (pWidth/2+paddleMargin) then -- Ball hit paddle
  260. if gameState[1] == "win" then -- On first paddle hit after clearing a board, load the new board
  261. gameState = {}
  262. boardReset()
  263. gameLevel = gameLevel + 1
  264. if gameLevel < 11 then
  265. ballVel:set(ballVel * 1.15) -- Speed up
  266. end
  267. pendingSound = 1
  268. else
  269. cheatPaddle()
  270. end
  271. bounce('y', -1, paddleMargin)
  272. end
  273. end
  274. -- Build out LED "screen". The screen always contains at least two digits and is least significant digit first
  275. local i, tempPoints = 1,points
  276. while i < 3 or tempPoints > 0 do -- Slice one digit off the points at a time
  277. screen[i] = tempPoints % 10
  278. tempPoints = math.floor(tempPoints/10)
  279. i = i + 1
  280. end
  281. if ballAt.y <= bKill + ballMargin then -- Ball has hit kill plane
  282. if DEBUG_backBumper then -- IMMORTALITY!!
  283. bounce('y', -1, bKill)
  284. else
  285. gameState = {"dead", start=tim}
  286. nextSound(ballXyz(ballAt), sounds.fail)
  287. screen = {11, 10} -- Special characters : (
  288. end
  289. end
  290. else -- Death sequence logic
  291. if not gameState.substate then -- After death, do nothing at all and show the :(
  292. if tim - gameState.start > gameStateDeathRollover.silence then
  293. gameState.substate = "silence"
  294. screen = {} -- Clear all digits to nil
  295. end
  296. elseif gameState.substate == "silence" then -- Then do nothing for a bit
  297. if tim - gameState.start > gameStateDeathRollover.tone then
  298. gameState.substate = "tone"
  299. nextSound(ballXyz(vec2(0,0)), sounds.restart)
  300. end
  301. end -- Then let the tone play out until "reset"
  302. end
  303. end
  304. local function drawLed(root, character) -- Draw one digit of the LED screen
  305. if not character then return end
  306. for y=1,led.height do
  307. local line = root
  308. for x=1,led.width do
  309. line = line + lRight
  310. if character[x][y] then
  311. ledCube(line)
  312. end
  313. end
  314. root = root + lDown
  315. end
  316. end
  317. function lovr.draw(eye)
  318. lovr.graphics.clear()
  319. if fixedCamera then
  320. lovr.graphics.translate(0, 0, -2) -- Move backward so Go users can see the paddle
  321. end
  322. -- This bit draws a three-dimensional grid, but it contains an intentional bug.
  323. -- The bug results in an interesting and attractive abstract environment!
  324. local gs = 30
  325. local far = 1*gs
  326. local grid = 2*gs
  327. for x=-grid,grid,gs do for y=-grid,grid,gs do for z=-grid,grid,gs do
  328. lovr.graphics.line(-far, y, z, far, y, z)
  329. if not (fixedCamera and x == 0 and z == 0) then -- In fixed camera setup this center line looks weird
  330. lovr.graphics.line(x, -far, z, x, far, z)
  331. end
  332. lovr.graphics.line(x, y, far, x, y, -far)
  333. end end end
  334. -- Draw board
  335. lovr.graphics.setShader(shader)
  336. local cheatX, cheatY, cheatStart
  337. if gameState.cheat then -- Unpack cheat state
  338. cheatX, cheatY, cheatStart = gameState.cheat.x, gameState.cheat.y, gameState.cheat.start
  339. end
  340. for x=1,bWidth do for y=1,bHeight do
  341. if Board.get(board,x,y) then
  342. if x == cheatX and y == cheatY and (tim - gameState.cheat.start) % 0.5 > 0.25 then -- Blinking cube during cheat
  343. lovr.graphics.setColor(1,1,1,1)
  344. else -- Base color on position so there's a nice gradient
  345. lovr.graphics.setColor(x/bWidth, y/bHeight, 1, 1)
  346. end
  347. cube(bCenter(x,y))
  348. end
  349. end end
  350. lovr.graphics.setColor(1, 1, 1, 1)
  351. -- Draw paddle and board
  352. paddle(paddleAt)
  353. if gameState[1] ~= "dead" then ball(ballAt) end
  354. -- Draw screen
  355. local screenlen = #screen
  356. local lUlCorner = lovr.math.vec3( lUlRoot - lDown*led.height - lRight * (screenlen * (led.width + 1) + 2)/ 2 )
  357. for i=screenlen,1, -1 do
  358. drawLed(lUlCorner, led[screen[i]+1])
  359. if i ~= 1 then lUlCorner = lUlCorner + lRight * (led.width + 1) end
  360. end
  361. -- Draw controller
  362. if controller then
  363. if controllerModel == nil then controllerModel = controller:newModel() or false end -- Only try to load controller once
  364. if controllerModel then
  365. local x, y, z, angle, ax, ay, az = lovr.headset.getPose(controller)
  366. controllerModel:draw(x,y,z,1,angle,ax, ay, az)
  367. end
  368. end
  369. end