nonogamer9

Untitled

Sep 26th, 2026 (edited)
186
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 30.10 KB | None | 0 0
  1. --[[
  2. MINECRAFT for OpenComputers (braille voxel raycaster)
  3. Ported from the ComputerCraft version.
  4.  
  5. - Braille rendering: every character cell = 2x4 dots, 2 colours per cell
  6. - Time-sliced rendering (never hits "too long without yielding")
  7. - VRAM double buffering (gpu.allocateBuffer + gpu.bitblt): frames are built
  8. off-screen and only the changed rows are blitted, so no tearing between slices
  9. - Diff-based cell updates + run-length batching of gpu.set calls
  10. - Sparse world (only edits are stored) + heightmap shading
  11. - Exact DDA block picking, sliding collision, held-key movement
  12. - Textures: downloaded BMPs, or procedural fallback if no internet card
  13. - Music: Computronics sound card (generative chiptune), optional
  14.  
  15. Controls
  16. W A S D move LShift sprint Space jump
  17. Arrows look Mouse drag look Scroll switch block
  18. E / B break / place 1 / 2 select block
  19. - / = lower / raise render quality
  20. M music on/off R random respawn
  21. Q quit
  22. ]]
  23.  
  24. local component = require("component")
  25. local computer = require("computer")
  26. local event = require("event")
  27. local keyboard = require("keyboard")
  28. local unicode = require("unicode")
  29. local fs = require("filesystem")
  30.  
  31. if not component.isAvailable("gpu") then error("A graphics card is required") end
  32. local gpu = component.gpu
  33. local K = keyboard.keys
  34.  
  35. local floor, abs, cos, sin = math.floor, math.abs, math.cos, math.sin
  36. local min, max, sqrt, ceil, random = math.min, math.max, math.sqrt, math.ceil, math.random
  37. local concat = table.concat
  38.  
  39. ---------------------------------------------------------------------
  40. -- Config
  41. ---------------------------------------------------------------------
  42. local ASSET_DIR = "/home/minecraft/"
  43. local ASSETS = {
  44. {"loading.bmp", "https://files.catbox.moe/i5nj03.bmp"},
  45. {"grass.bmp", "https://files.catbox.moe/x7tf9n.bmp"},
  46. {"stone.bmp", "https://files.catbox.moe/zs25m6.bmp"},
  47. }
  48.  
  49. -- cols/rows are clipped to what your GPU/screen tier supports.
  50. -- sx*sy = rays per character cell (2x4 would be one ray per braille dot).
  51. local PRESETS = {
  52. {name = "Fast", cols = 80, rows = 25, sx = 1, sy = 2},
  53. {name = "Balanced", cols = 80, rows = 25, sx = 2, sy = 2},
  54. {name = "Sharp", cols = 80, rows = 25, sx = 2, sy = 4},
  55. {name = "Ultra", cols = 160, rows = 50, sx = 2, sy = 2},
  56. }
  57. local quality = 2
  58. local VIEW_DIST = 48 -- max DDA steps per ray
  59. local FOV = 1.1
  60. local SLICE_IDLE = 0.25 -- CPU seconds per render slice when standing still
  61. local SLICE_MOVE = 0.08 -- shorter slices while moving => input polled more often
  62. local LOWCONTRAST = 10 -- cells flatter than this become a solid colour
  63.  
  64. local GRAV, JUMP, WALK, SPRINT, TURN = -20.0, 7.5, 4.3, 7.0, 1.9
  65.  
  66. ---------------------------------------------------------------------
  67. -- Helpers
  68. ---------------------------------------------------------------------
  69. local function clamp(v, a, b) if v < a then return a elseif v > b then return b end return v end
  70. local function pack(r, g, b) return floor(r + 0.5) * 65536 + floor(g + 0.5) * 256 + floor(b + 0.5) end
  71.  
  72. ---------------------------------------------------------------------
  73. -- Textures
  74. ---------------------------------------------------------------------
  75. local function u16(s, o) local a, b = s:byte(o, o + 1) return a + b * 256 end
  76. local function u32(s, o) local a, b, c, d = s:byte(o, o + 3) return a + b * 256 + c * 65536 + d * 16777216 end
  77.  
  78. -- Loads a 24/32-bit BMP, optionally subsampled so max(w,h) <= maxDim.
  79. local function loadBMP(path, maxDim)
  80. local f = io.open(path, "rb")
  81. if not f then return nil end
  82. local data = f:read("*a")
  83. f:close()
  84. if not data or #data < 54 or data:sub(1, 2) ~= "BM" then return nil end
  85. local off = u32(data, 11) + 1
  86. local w = u32(data, 19)
  87. local hraw = u32(data, 23)
  88. local bpp = u16(data, 29)
  89. local comp = u32(data, 31)
  90. local topDown = false
  91. if hraw >= 2147483648 then hraw = 4294967296 - hraw; topDown = true end
  92. local h = hraw
  93. if (bpp ~= 24 and bpp ~= 32) or (comp ~= 0 and comp ~= 3) or w < 1 or h < 1 then return nil end
  94. local bytes = bpp / 8
  95. local stride = floor((bpp * w + 31) / 32) * 4
  96. local step = max(1, ceil(max(w, h) / (maxDim or 128)))
  97. local ow, oh = floor((w + step - 1) / step), floor((h + step - 1) / step)
  98. local px = {}
  99. for oy = 0, oh - 1 do
  100. local y = oy * step -- top-down row in the image
  101. local row = topDown and y or (h - 1 - y) -- row index inside the file
  102. local base = off + row * stride
  103. for ox = 0, ow - 1 do
  104. local o = base + ox * step * bytes
  105. local b, g, r = data:byte(o, o + 2)
  106. px[oy * ow + ox + 1] = r * 65536 + g * 256 + b
  107. end
  108. end
  109. return {w = ow, h = oh, px = px}
  110. end
  111.  
  112. local function genTexture(kind)
  113. local w, h, px = 16, 16, {}
  114. local seed = kind == 1 and 1234 or 4321
  115. local function rnd() seed = (seed * 1103515245 + 12345) % 2147483648 return seed / 2147483648 end
  116. for i = 1, w * h do
  117. local n = rnd()
  118. if kind == 1 then px[i] = pack(55 + n * 35, 135 + n * 60, 40 + n * 30)
  119. else local g = 105 + n * 55; px[i] = pack(g, g, g + 4) end
  120. end
  121. return {w = w, h = h, px = px}
  122. end
  123.  
  124. -- 4 pre-shaded variants so rendering is a single table lookup:
  125. -- 1 lit top, 2 lit side, 3 shaded (something above), 4 highlighted
  126. local function makeVariants(tex)
  127. local v1, v2, v3, v4 = {}, {}, {}, {}
  128. for i = 1, tex.w * tex.h do
  129. local c = tex.px[i]
  130. local r, g, b = floor(c / 65536), floor(c / 256) % 256, c % 256
  131. v1[i] = c
  132. v2[i] = pack(r * 0.8, g * 0.8, b * 0.8)
  133. v3[i] = pack(r * 0.42, g * 0.42, b * 0.46)
  134. v4[i] = pack(r + (255 - r) * 0.45, g + (255 - g) * 0.45, b + (255 - b) * 0.2)
  135. end
  136. tex.v = {v1, v2, v3, v4}
  137. end
  138.  
  139. local function download(url, path)
  140. if fs.exists(path) then return true end
  141. if not component.isAvailable("internet") then return false end
  142. local ok = pcall(function()
  143. local internet = require("internet")
  144. local f = assert(io.open(path, "wb"))
  145. local good, err = pcall(function()
  146. for chunk in internet.request(url) do f:write(chunk) end
  147. end)
  148. f:close()
  149. if not good then fs.remove(path) error(err) end
  150. end)
  151. return ok and fs.exists(path)
  152. end
  153.  
  154. local textures = {}
  155. local BLOCK_NAMES = {"Grass", "Stone"}
  156. local selectedBlock = 1
  157.  
  158. ---------------------------------------------------------------------
  159. -- World: sparse edits over an infinite-feeling flat base
  160. -- y<4 stone(2), y==4 grass(1), above = air
  161. ---------------------------------------------------------------------
  162. local MAP_W, MAP_H, MAP_D = 256, 64, 256
  163. local edits, colTop, topY = {}, {}, 4
  164. local base = {}
  165. for y = 0, MAP_H - 1 do base[y] = (y == 4 and 1) or (y < 4 and 2) or 0 end
  166.  
  167. local function setBlock(x, y, z, b)
  168. local ck = z * 256 + x
  169. local key = y * 65536 + ck
  170. if b == base[y] then edits[key] = nil else edits[key] = b end
  171. if b > 0 then
  172. if y > (colTop[ck] or 4) then colTop[ck] = y end
  173. if y > topY then topY = y end
  174. elseif y == (colTop[ck] or 4) then
  175. local ny = y - 1
  176. while ny >= 0 and (edits[ny * 65536 + ck] or base[ny]) == 0 do ny = ny - 1 end
  177. colTop[ck] = ny
  178. end
  179. end
  180.  
  181. local function collides(x, y, z)
  182. for ix = floor(x - 0.3), floor(x + 0.3) do
  183. for iz = floor(z - 0.3), floor(z + 0.3) do
  184. if ix < 0 or ix >= MAP_W or iz < 0 or iz >= MAP_D then return true end
  185. for iy = floor(y - 1.4), floor(y) do
  186. if iy >= 0 and iy < MAP_H and (edits[iy * 65536 + iz * 256 + ix] or base[iy]) > 0 then
  187. return true
  188. end
  189. end
  190. end
  191. end
  192. return false
  193. end
  194.  
  195. ---------------------------------------------------------------------
  196. -- Player state
  197. ---------------------------------------------------------------------
  198. local camX, camY, camZ = 128, 6.5, 128
  199. local lookDir, pitch = 0, 0
  200. local velY, onGround = 0, false
  201. local held = {}
  202. local dirty, running = true, true
  203.  
  204. local function respawn()
  205. camX, camZ, camY, velY = random(20, 230), random(20, 230), 15, 0
  206. dirty = true
  207. end
  208.  
  209. local lastLook = -10
  210. local moveKeys = {K.w, K.a, K.s, K.d, K.left, K.right, K.up, K.down, K.space}
  211. local function isMoving()
  212. if computer.uptime() - lastLook < 0.3 or not onGround then return true end
  213. for i = 1, #moveKeys do if held[moveKeys[i]] then return true end end
  214. return false
  215. end
  216.  
  217. local function physics(dt)
  218. dt = min(dt, 0.25)
  219. local n = max(1, ceil(dt / 0.025))
  220. local h = dt / n
  221. local cL, sL = cos(lookDir), sin(lookDir)
  222. local mx, mz = 0, 0
  223. if held[K.w] then mx, mz = mx + cL, mz + sL end
  224. if held[K.s] then mx, mz = mx - cL, mz - sL end
  225. if held[K.a] then mx, mz = mx + sL, mz - cL end
  226. if held[K.d] then mx, mz = mx - sL, mz + cL end
  227. local moving = (mx ~= 0 or mz ~= 0)
  228. if moving then
  229. local sp = (held[K.lshift] and SPRINT or WALK) / sqrt(mx * mx + mz * mz)
  230. mx, mz = mx * sp, mz * sp
  231. end
  232. local turn = (held[K.right] and 1 or 0) - (held[K.left] and 1 or 0)
  233. if turn ~= 0 then lookDir = lookDir + turn * TURN * dt; dirty = true end
  234. local tilt = (held[K.up] and 1 or 0) - (held[K.down] and 1 or 0)
  235. if tilt ~= 0 then pitch = clamp(pitch + tilt * TURN * 0.8 * dt, -1.4, 1.4); dirty = true end
  236. local wantJump = held[K.space]
  237.  
  238. for _ = 1, n do
  239. if moving then
  240. local nx = camX + mx * h
  241. if not collides(nx, camY, camZ) then camX = nx; dirty = true end
  242. local nz = camZ + mz * h
  243. if not collides(camX, camY, nz) then camZ = nz; dirty = true end
  244. end
  245. if wantJump and onGround then velY = JUMP; onGround = false end
  246. velY = velY + GRAV * h
  247. local ny = camY + velY * h
  248. if collides(camX, ny, camZ) then
  249. if velY < 0 then onGround = true end
  250. velY = 0
  251. else
  252. camY = ny; onGround = false; dirty = true
  253. end
  254. end
  255. if camY < -20 then respawn() end
  256. end
  257.  
  258. ---------------------------------------------------------------------
  259. -- Block picking (exact DDA). Returns hit cell + the empty cell before it.
  260. ---------------------------------------------------------------------
  261. local function interactRay(maxDist)
  262. local cL, sL, cP, sP = cos(lookDir), sin(lookDir), cos(pitch), sin(pitch)
  263. local rdx, rdy, rdz = cL * cP, sP, sL * cP
  264. local ax, ay, az = abs(rdx), abs(rdy), abs(rdz)
  265. if ax < 1e-9 then ax = 1e-9 end
  266. if ay < 1e-9 then ay = 1e-9 end
  267. if az < 1e-9 then az = 1e-9 end
  268. local dX, dY, dZ = 1 / ax, 1 / ay, 1 / az
  269. local sX, sY, sZ = rdx < 0 and -1 or 1, rdy < 0 and -1 or 1, rdz < 0 and -1 or 1
  270. local mX, mY, mZ = floor(camX), floor(camY), floor(camZ)
  271. local sdX = rdx < 0 and (camX - mX) * dX or (mX + 1 - camX) * dX
  272. local sdY = rdy < 0 and (camY - mY) * dY or (mY + 1 - camY) * dY
  273. local sdZ = rdz < 0 and (camZ - mZ) * dZ or (mZ + 1 - camZ) * dZ
  274. for _ = 1, 64 do
  275. local pX, pY, pZ = mX, mY, mZ
  276. local t
  277. if sdX < sdY then
  278. if sdX < sdZ then t = sdX; sdX = sdX + dX; mX = mX + sX
  279. else t = sdZ; sdZ = sdZ + dZ; mZ = mZ + sZ end
  280. elseif sdY < sdZ then t = sdY; sdY = sdY + dY; mY = mY + sY
  281. else t = sdZ; sdZ = sdZ + dZ; mZ = mZ + sZ end
  282. if t > maxDist then return nil end
  283. if mX < 0 or mX >= MAP_W or mZ < 0 or mZ >= MAP_D or mY < 0 or mY >= MAP_H then return nil end
  284. if (edits[mY * 65536 + mZ * 256 + mX] or base[mY]) > 0 then return mX, mY, mZ, pX, pY, pZ end
  285. end
  286. end
  287.  
  288. local function breakBlock()
  289. local hx, hy, hz = interactRay(8)
  290. if hx then setBlock(hx, hy, hz, 0); dirty = true end
  291. end
  292.  
  293. local function placeBlock()
  294. local hx, hy, hz, px, py, pz = interactRay(8)
  295. if not hx then return end
  296. if px < 0 or px >= MAP_W or pz < 0 or pz >= MAP_D or py < 0 or py >= MAP_H then return end
  297. setBlock(px, py, pz, selectedBlock)
  298. if collides(camX, camY, camZ) then setBlock(px, py, pz, 0) return end -- don't entomb yourself
  299. dirty = true
  300. end
  301.  
  302. ---------------------------------------------------------------------
  303. -- Colour handling for braille cells
  304. ---------------------------------------------------------------------
  305. local depth = gpu.getDepth()
  306. local rLev, gLev, bLev = {}, {}, {}
  307. for i = 0, 5 do rLev[i] = floor(i * 255 / 5) end
  308. for i = 0, 7 do gLev[i] = floor(i * 255 / 7) end
  309. for i = 0, 4 do bLev[i] = floor(i * 255 / 4) end
  310. -- snap to the tier-3 colour cube so equal colours really are equal on screen
  311. local function qCube(r, g, b)
  312. return rLev[floor(r * 5 / 255 + 0.5)] * 65536 + gLev[floor(g * 7 / 255 + 0.5)] * 256 + bLev[floor(b * 4 / 255 + 0.5)]
  313. end
  314. local function qPlain(r, g, b) return pack(r, g, b) end
  315. local quant = (depth == 8) and qCube or qPlain
  316.  
  317. local Rc, Gc, Bc, Lc, solidMemo = {}, {}, {}, {}, {}
  318. local function cacheColor(c)
  319. local r, g, b = floor(c / 65536), floor(c / 256) % 256, c % 256
  320. Rc[c], Gc[c], Bc[c] = r, g, b
  321. Lc[c] = 0.299 * r + 0.587 * g + 0.114 * b
  322. end
  323.  
  324. local dots, lums, samp, dmap = {0, 0, 0, 0, 0, 0, 0, 0}, {}, {}, {}
  325. -- dot k (1..8) = dy*2+dx+1 -> braille bit
  326. local bitOf = {1, 8, 2, 16, 4, 32, 64, 128}
  327. local chars = {[0] = " "}
  328. for b = 1, 255 do chars[b] = unicode.char(0x2800 + b) end
  329.  
  330. -- Split the 8 dot colours into (bright -> ink, dark -> background)
  331. local function cluster()
  332. local c1 = dots[1]
  333. if c1 == dots[2] and c1 == dots[3] and c1 == dots[4] and c1 == dots[5]
  334. and c1 == dots[6] and c1 == dots[7] and c1 == dots[8] then
  335. local q = solidMemo[c1]
  336. if not q then
  337. if not Lc[c1] then cacheColor(c1) end
  338. q = quant(Rc[c1], Gc[c1], Bc[c1]); solidMemo[c1] = q
  339. end
  340. return q, q, 0
  341. end
  342. local sum = 0
  343. for k = 1, 8 do
  344. local c = dots[k]
  345. local l = Lc[c]
  346. if not l then cacheColor(c); l = Lc[c] end
  347. lums[k] = l; sum = sum + l
  348. end
  349. local mean = sum * 0.125
  350. local fr, fg, fb, nf, fl = 0, 0, 0, 0, 0
  351. local br, bg, bb, nb, bl = 0, 0, 0, 0, 0
  352. local bits = 0
  353. for k = 1, 8 do
  354. local c, l = dots[k], lums[k]
  355. if l > mean then
  356. bits = bits + bitOf[k]
  357. fr = fr + Rc[c]; fg = fg + Gc[c]; fb = fb + Bc[c]; nf = nf + 1; fl = fl + l
  358. else
  359. br = br + Rc[c]; bg = bg + Gc[c]; bb = bb + Bc[c]; nb = nb + 1; bl = bl + l
  360. end
  361. end
  362. if nf == 0 or nb == 0 or fl / nf - bl / nb < LOWCONTRAST then
  363. local q = quant((fr + br) * 0.125, (fg + bg) * 0.125, (fb + bb) * 0.125)
  364. return q, q, 0
  365. end
  366. local f = quant(fr / nf, fg / nf, fb / nf)
  367. local b = quant(br / nb, bg / nb, bb / nb)
  368. if f == b then return b, b, 0 end
  369. return f, b, bits
  370. end
  371.  
  372. ---------------------------------------------------------------------
  373. -- Screen state + diffed, run-batched output
  374. ---------------------------------------------------------------------
  375. local cols, rows, gridRows, sx, sy, aspect = 80, 25, 24, 2, 2, 1
  376. local pfg, pbg, pbits = {}, {}, {}
  377. local curFg, curBg
  378. local runN, runX, runY, runFg, runBg, runInk, runBuf = 0, 0, 0, 0, 0, false, {}
  379. local baseSx, baseSy = 2, 2 -- preset sampling; passLod = this pass used the cheap sampling
  380. local passLod = false
  381. local backBuf -- VRAM back buffer index (nil = draw straight to screen)
  382. local minRow, maxRow = 1e9, -1 -- rows touched during the current pass (for partial bitblt)
  383.  
  384. local function flush()
  385. if runN > 0 then
  386. if runInk and runFg ~= curFg then gpu.setForeground(runFg); curFg = runFg end
  387. if runBg ~= curBg then gpu.setBackground(runBg); curBg = runBg end
  388. gpu.set(runX, runY, concat(runBuf, "", 1, runN))
  389. runN = 0
  390. end
  391. end
  392.  
  393. local function emitRow(row, fill)
  394. local y, rowBase = row + 1, row * cols
  395. for col = 0, cols - 1 do
  396. fill(row, col)
  397. local fg, bg, bits = cluster()
  398. local idx = rowBase + col + 1
  399. if pfg[idx] == fg and pbg[idx] == bg and pbits[idx] == bits then
  400. flush()
  401. else
  402. pfg[idx], pbg[idx], pbits[idx] = fg, bg, bits
  403. if row < minRow then minRow = row end
  404. if row > maxRow then maxRow = row end
  405. -- blank cells can ride along in any run with the same background
  406. if runN > 0 and runBg == bg and (bits == 0 or not runInk or runFg == fg) then
  407. runN = runN + 1; runBuf[runN] = chars[bits]
  408. if bits ~= 0 and not runInk then runFg = fg; runInk = true end
  409. else
  410. flush()
  411. runN, runX, runY, runFg, runBg, runInk = 1, col + 1, y, fg, bg, (bits ~= 0)
  412. runBuf[1] = chars[bits]
  413. end
  414. end
  415. end
  416. flush()
  417. end
  418.  
  419. local function setColors(fg, bg)
  420. if fg ~= curFg then gpu.setForeground(fg); curFg = fg end
  421. if bg ~= curBg then gpu.setBackground(bg); curBg = bg end
  422. end
  423.  
  424. -- Colour state is tracked per active buffer, so reset the trackers on every switch.
  425. local function activate(idx)
  426. gpu.setActiveBuffer(idx)
  427. curFg, curBg = nil, nil
  428. end
  429.  
  430. local function allocBack()
  431. if not (gpu.allocateBuffer and gpu.setActiveBuffer and gpu.bitblt) then return nil end
  432. local ok, idx = pcall(gpu.allocateBuffer, cols, gridRows)
  433. if ok and idx then return idx end
  434. return nil -- not enough VRAM (or old OC): fall back to direct drawing
  435. end
  436.  
  437. -- Clears the screen and the back buffer and forgets the diff cache.
  438. -- Leaves the back buffer (if any) as the active draw target.
  439. local function resetFrame()
  440. pfg, pbg, pbits = {}, {}, {}
  441. if backBuf then activate(0) end
  442. curFg, curBg = nil, nil
  443. setColors(0xFFFFFF, 0x000000)
  444. gpu.fill(1, 1, cols, rows, " ")
  445. if backBuf then
  446. activate(backBuf)
  447. setColors(0xFFFFFF, 0x000000)
  448. gpu.fill(1, 1, cols, gridRows, " ")
  449. end
  450. end
  451.  
  452. -- Copies only the rows that changed this pass from VRAM to the screen in one call.
  453. local function presentFrame()
  454. if backBuf and maxRow >= minRow then
  455. gpu.bitblt(0, 1, minRow + 1, cols, maxRow - minRow + 1, backBuf, 1, minRow + 1)
  456. end
  457. minRow, maxRow = 1e9, -1
  458. end
  459.  
  460. -- Draw text directly on the real screen (HUD), then go back to the back buffer.
  461. local function screenDraw(fn)
  462. if backBuf then activate(0) end
  463. fn()
  464. if backBuf then activate(backBuf) end
  465. end
  466.  
  467. local function setSampling(a, b)
  468. sx, sy = a, b
  469. for k = 1, 8 do
  470. local dx, dy = (k - 1) % 2, floor((k - 1) / 2)
  471. dmap[k] = floor(dy * sy / 4) * sx + floor(dx * sx / 2) + 1
  472. end
  473. end
  474.  
  475. local function applyQuality()
  476. if backBuf then
  477. pcall(gpu.setActiveBuffer, 0)
  478. pcall(gpu.freeBuffer, backBuf)
  479. backBuf = nil
  480. end
  481. local p = PRESETS[quality]
  482. local maxW, maxH = gpu.maxResolution()
  483. cols, rows = min(p.cols, maxW), min(p.rows, maxH)
  484. gpu.setResolution(cols, rows)
  485. gridRows = rows - 1 -- last row = HUD
  486. baseSx, baseSy = p.sx, p.sy
  487. setSampling(baseSx, baseSy)
  488. aspect = 2 * gridRows / cols -- braille dots are square
  489. backBuf = allocBack()
  490. minRow, maxRow = 1e9, -1
  491. resetFrame()
  492. end
  493.  
  494. ---------------------------------------------------------------------
  495. -- Raycaster (one ray -> one packed RGB colour)
  496. ---------------------------------------------------------------------
  497. local sCX, sCY, sCZ, fX, fY, fZ = 0, 0, 0, 0, 0, 0 -- per-pass camera snapshot
  498. local hlX, hlY, hlZ -- highlighted block
  499.  
  500. local function castRay(rdx, rdy, rdz, skyc)
  501. local ax = rdx < 0 and -rdx or rdx
  502. local ay = rdy < 0 and -rdy or rdy
  503. local az = rdz < 0 and -rdz or rdz
  504. if ax < 1e-9 then ax = 1e-9 end
  505. if ay < 1e-9 then ay = 1e-9 end
  506. if az < 1e-9 then az = 1e-9 end
  507. local dX, dY, dZ = 1 / ax, 1 / ay, 1 / az
  508. local sX, sY, sZ = rdx < 0 and -1 or 1, rdy < 0 and -1 or 1, rdz < 0 and -1 or 1
  509. local mX, mY, mZ = fX, fY, fZ
  510. local sdX = rdx < 0 and (sCX - mX) * dX or (mX + 1 - sCX) * dX
  511. local sdY = rdy < 0 and (sCY - mY) * dY or (mY + 1 - sCY) * dY
  512. local sdZ = rdz < 0 and (sCZ - mZ) * dZ or (mZ + 1 - sCZ) * dZ
  513. local up, top = rdy > 0, topY
  514. local ed, bs, ct = edits, base, colTop
  515.  
  516. for _ = 1, VIEW_DIST do
  517. local t, side
  518. if sdX < sdY then
  519. if sdX < sdZ then t = sdX; sdX = sdX + dX; mX = mX + sX; side = 0
  520. else t = sdZ; sdZ = sdZ + dZ; mZ = mZ + sZ; side = 2 end
  521. elseif sdY < sdZ then t = sdY; sdY = sdY + dY; mY = mY + sY; side = 1
  522. else t = sdZ; sdZ = sdZ + dZ; mZ = mZ + sZ; side = 2 end
  523.  
  524. if mX < 0 or mX >= MAP_W or mZ < 0 or mZ >= MAP_D or mY < 0 or mY >= MAP_H then return skyc end
  525. if up and mY > top then return skyc end -- nothing can be above the tallest block
  526.  
  527. local b = ed[mY * 65536 + mZ * 256 + mX] or bs[mY]
  528. if b > 0 then
  529. local tex = textures[b]
  530. local tw, th = tex.w, tex.h
  531. local hx, hy, hz = sCX + t * rdx, sCY + t * rdy, sCZ + t * rdz
  532. local u, v
  533. if side == 0 then u, v = hz - floor(hz), hy - floor(hy)
  534. elseif side == 1 then u, v = hx - floor(hx), hz - floor(hz)
  535. else u, v = hx - floor(hx), hy - floor(hy) end
  536. local tx = floor(u * tw); if tx >= tw then tx = tw - 1 end
  537. local ty = floor((1 - v) * th); if ty >= th then ty = th - 1 end
  538. local var
  539. if mX == hlX and mY == hlY and mZ == hlZ then var = 4
  540. elseif (ct[mZ * 256 + mX] or 4) >= mY + 2 then var = 3 -- covered from above
  541. elseif side ~= 1 then var = 2
  542. else var = 1 end
  543. return tex.v[var][ty * tw + tx + 1]
  544. end
  545. end
  546. return skyc
  547. end
  548.  
  549. local function skyFor(ry)
  550. local t = clamp(ry * 1.2, 0, 1)
  551. return pack(170 + (60 - 170) * t, 220 + (140 - 220) * t, 255 + (235 - 255) * t)
  552. end
  553.  
  554. local colAx, colAz, rowBx, rowBz, rowRy, rowSky = {}, {}, {}, {}, {}, {}
  555. local passActive, passRow, passStart = false, 0, 0
  556. local fps = 0
  557.  
  558. local function beginPass()
  559. -- while moving, cast fewer rays per cell (1x2); refine at full quality once you stop
  560. passLod = (baseSx > 1 or baseSy > 2) and isMoving()
  561. if passLod then setSampling(1, 2) else setSampling(baseSx, baseSy) end
  562. sCX, sCY, sCZ = camX, camY, camZ
  563. fX, fY, fZ = floor(sCX), floor(sCY), floor(sCZ)
  564. local cL, sL, cP, sP = cos(lookDir), sin(lookDir), cos(pitch), sin(pitch)
  565. hlX, hlY, hlZ = interactRay(8)
  566. local scols, srows = cols * sx, gridRows * sy
  567. for sc = 0, scols - 1 do
  568. local vX = ((sc + 0.5) / scols - 0.5) * FOV
  569. colAx[sc] = cL * cP - vX * sL
  570. colAz[sc] = sL * cP + vX * cL
  571. end
  572. for sr = 0, srows - 1 do
  573. local vY = (0.5 - (sr + 0.5) / srows) * FOV * aspect
  574. rowBx[sr] = vY * cL * sP
  575. rowBz[sr] = vY * sL * sP
  576. local ry = sP + vY * cP
  577. rowRy[sr] = ry
  578. rowSky[sr] = skyFor(ry)
  579. end
  580. passRow, passActive, passStart = 0, true, computer.uptime()
  581. minRow, maxRow = 1e9, -1
  582. end
  583.  
  584. local function gameCell(row, col)
  585. local n, sr0, sc0 = 0, row * sy, col * sx
  586. for j = 0, sy - 1 do
  587. local sr = sr0 + j
  588. local bx, bz, ry, sk = rowBx[sr], rowBz[sr], rowRy[sr], rowSky[sr]
  589. for i = 0, sx - 1 do
  590. local sc = sc0 + i
  591. n = n + 1
  592. samp[n] = castRay(colAx[sc] - bx, ry, colAz[sc] - bz, sk)
  593. end
  594. end
  595. for k = 1, 8 do dots[k] = samp[dmap[k]] end
  596. end
  597.  
  598. local function drawHUD()
  599. local p = PRESETS[quality]
  600. local s = string.format(" %s | Q%d %s | %.1f fps | %s | %d %d %d",
  601. BLOCK_NAMES[selectedBlock], quality, p.name, fps, backBuf and "VRAM" or "direct",
  602. floor(camX), floor(camY), floor(camZ))
  603. s = s:sub(1, cols)
  604. s = s .. string.rep(" ", cols - #s)
  605. screenDraw(function()
  606. setColors(0xFFFFFF, 0x000000)
  607. gpu.set(1, rows, s)
  608. -- crosshair on the real screen (the blit overwrites it each pass, so no cache issues)
  609. local cx, cy = floor(cols / 2), floor(gridRows / 2)
  610. setColors(0xFFFFFF, pbg[cy * cols + cx + 1] or 0x000000)
  611. gpu.set(cx + 1, cy + 1, "+")
  612. end)
  613. end
  614.  
  615. -- Renders rows until the CPU-time budget is used; returns true when the pass is complete.
  616. local function renderSlice(budget)
  617. local t0 = os.clock()
  618. while passRow < gridRows do
  619. emitRow(passRow, gameCell)
  620. passRow = passRow + 1
  621. if os.clock() - t0 > budget then break end
  622. end
  623. if passRow >= gridRows then passActive = false return true end
  624. return false
  625. end
  626.  
  627. ---------------------------------------------------------------------
  628. -- Music (Computronics sound card, best effort)
  629. ---------------------------------------------------------------------
  630. local sound = component.isAvailable("sound") and component.sound or nil
  631. local musicOn = sound ~= nil
  632. local nextMusic = 0
  633. local NOTE_MS = 280
  634. local melody = {261.63, 293.66, 329.63, 392.00, 440.00, 523.25, 587.33, 659.25}
  635. local bassNotes = {130.81, 98.00, 110.00, 87.31}
  636. local mIdx, bIdx = 3, 1
  637.  
  638. local function musicInit()
  639. if not sound then return end
  640. pcall(function() sound.setTotalVolume(0.6) end)
  641. pcall(function() sound.setWave(1, sound.modes.sine) end)
  642. pcall(function() sound.setWave(2, sound.modes.triangle) end)
  643. pcall(function() sound.setVolume(1, 0.35) end)
  644. pcall(function() sound.setVolume(2, 0.45) end)
  645. end
  646.  
  647. local function musicStop()
  648. if not sound then return end
  649. pcall(function() sound.close(1); sound.close(2); sound.clear() end)
  650. end
  651.  
  652. local function musicTick(now)
  653. if not musicOn or not sound or now < nextMusic then return end
  654. local steps = 8
  655. pcall(function()
  656. for step = 1, steps do
  657. if step % 4 == 1 then
  658. sound.setFrequency(2, bassNotes[bIdx]); sound.open(2)
  659. bIdx = bIdx % #bassNotes + 1
  660. end
  661. if random() > 0.2 then
  662. mIdx = clamp(mIdx + random(-2, 2), 1, #melody)
  663. sound.setFrequency(1, melody[mIdx]); sound.open(1)
  664. sound.delay(NOTE_MS - 50); sound.close(1); sound.delay(50)
  665. else
  666. sound.delay(NOTE_MS)
  667. end
  668. if step % 4 == 0 then sound.close(2) end
  669. end
  670. sound.process()
  671. end)
  672. nextMusic = now + steps * NOTE_MS / 1000 - 0.1
  673. end
  674.  
  675. ---------------------------------------------------------------------
  676. -- Input (event listeners keep working even while the renderer is yielding)
  677. ---------------------------------------------------------------------
  678. local lastMX, lastMY
  679.  
  680. local function onKeyDown(_, _, _, code)
  681. if held[code] then return end -- ignore OS key-repeat for one-shot actions
  682. held[code] = true
  683. if code == K.q then running = false
  684. elseif code == K["1"] then selectedBlock = 1; dirty = true
  685. elseif code == K["2"] then selectedBlock = 2; dirty = true
  686. elseif code == K.r then respawn()
  687. elseif code == K.e then breakBlock()
  688. elseif code == K.b then placeBlock()
  689. elseif code == K.m then
  690. musicOn = sound ~= nil and not musicOn
  691. if not musicOn then musicStop() else nextMusic = 0 end
  692. elseif code == K.minus or code == K.equals then
  693. local nq = clamp(quality + (code == K.equals and 1 or -1), 1, #PRESETS)
  694. if nq ~= quality then
  695. quality = nq
  696. passActive = false
  697. applyQuality()
  698. dirty = true
  699. end
  700. end
  701. end
  702.  
  703. local function onKeyUp(_, _, _, code) held[code] = nil end
  704. local function onTouch(_, _, x, y) lastMX, lastMY = x, y end
  705. local function onDrag(_, _, x, y)
  706. lastLook = computer.uptime()
  707. if lastMX then
  708. lookDir = lookDir + (x - lastMX) * 0.05
  709. pitch = clamp(pitch + (lastMY - y) * 0.07, -1.4, 1.4)
  710. dirty = true
  711. end
  712. lastMX, lastMY = x, y
  713. end
  714. local function onDrop() lastMX, lastMY = nil, nil end
  715. local function onScroll(_, _, _, _, dir)
  716. selectedBlock = ((selectedBlock - 1 + (dir > 0 and -1 or 1)) % #BLOCK_NAMES) + 1
  717. dirty = true
  718. end
  719.  
  720. local listeners = {
  721. key_down = onKeyDown, key_up = onKeyUp, touch = onTouch,
  722. drag = onDrag, drop = onDrop, scroll = onScroll,
  723. }
  724.  
  725. ---------------------------------------------------------------------
  726. -- Main
  727. ---------------------------------------------------------------------
  728. -- Yield once, then drain EVERY queued signal. (os.sleep(0) only consumes one signal per
  729. -- tick, so key-repeat/drag floods backed up and key_up arrived late => "sticky" controls.)
  730. local function pump(timeout)
  731. local name = event.pull(timeout) -- listeners fire for each signal pulled
  732. local n = 0
  733. while name and n < 300 do
  734. name = event.pull(0)
  735. n = n + 1
  736. end
  737. end
  738.  
  739. local function showLoading(img)
  740. applyQuality()
  741. if img then
  742. local function fill(row, col)
  743. for k = 1, 8 do
  744. local dx, dy = (k - 1) % 2, floor((k - 1) / 2)
  745. local u = (col + (dx + 0.5) / 2) / cols
  746. local v = (row + (dy + 0.5) / 4) / gridRows
  747. dots[k] = img.px[floor(v * img.h) * img.w + floor(u * img.w) + 1]
  748. end
  749. end
  750. for row = 0, gridRows - 1 do emitRow(row, fill) end
  751. presentFrame()
  752. end
  753. local help = " WASD move | Arrows/drag look | Space jump | E break | B place | 1/2 block | -/= quality | M music | Q quit - press a key"
  754. screenDraw(function()
  755. setColors(0xFFFFFF, 0x000000)
  756. gpu.set(1, rows, help:sub(1, cols) .. string.rep(" ", max(0, cols - #help)))
  757. end)
  758. event.pull("key_down")
  759. end
  760.  
  761. local function main()
  762. if not fs.exists(ASSET_DIR) then fs.makeDirectory(ASSET_DIR) end
  763. for _, a in ipairs(ASSETS) do
  764. if not fs.exists(ASSET_DIR .. a[1]) then
  765. print("Fetching " .. a[1] .. " ...")
  766. if not download(a[2], ASSET_DIR .. a[1]) then print(" (unavailable, using fallback)") end
  767. end
  768. end
  769. local function try(name, maxDim)
  770. local ok, res = pcall(loadBMP, ASSET_DIR .. name, maxDim)
  771. return ok and res or nil
  772. end
  773. textures[1] = try("grass.bmp", 128) or genTexture(1)
  774. textures[2] = try("stone.bmp", 128) or genTexture(2)
  775. makeVariants(textures[1]); makeVariants(textures[2])
  776. local loadingImg = try("loading.bmp", 320)
  777.  
  778. musicInit()
  779. showLoading(loadingImg)
  780. resetFrame()
  781.  
  782. for name, fn in pairs(listeners) do event.listen(name, fn) end
  783.  
  784. local last = computer.uptime()
  785. dirty = true
  786. while running do
  787. local now = computer.uptime()
  788. physics(now - last)
  789. last = now
  790. musicTick(now)
  791.  
  792. if dirty and not passActive then dirty = false; beginPass() end
  793. if passActive then
  794. if renderSlice(isMoving() and SLICE_MOVE or SLICE_IDLE) then
  795. local dur = max(computer.uptime() - passStart, 0.05)
  796. fps = fps == 0 and 1 / dur or (fps * 0.7 + 0.3 / dur)
  797. presentFrame()
  798. drawHUD()
  799. if passLod and not isMoving() then dirty = true end -- refine at full quality
  800. end
  801. pump(0)
  802. else
  803. pump(0.05)
  804. end
  805. end
  806. end
  807.  
  808. local oW, oH = gpu.getResolution()
  809. local ok, err = pcall(main)
  810.  
  811. for name, fn in pairs(listeners) do pcall(event.ignore, name, fn) end
  812. musicStop()
  813. pcall(function() gpu.setActiveBuffer(0); gpu.freeAllBuffers() end)
  814. gpu.setResolution(oW, oH)
  815. gpu.setBackground(0x000000)
  816. gpu.setForeground(0xFFFFFF)
  817. gpu.fill(1, 1, oW, oH, " ")
  818. pcall(function() require("term").setCursor(1, 1) end)
  819. if not ok and not tostring(err):find("interrupted") then error(err, 0) end
Add Comment
Please, Sign In to add comment