Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --[[
- MINECRAFT for OpenComputers (braille voxel raycaster)
- Ported from the ComputerCraft version.
- - Braille rendering: every character cell = 2x4 dots, 2 colours per cell
- - Time-sliced rendering (never hits "too long without yielding")
- - VRAM double buffering (gpu.allocateBuffer + gpu.bitblt): frames are built
- off-screen and only the changed rows are blitted, so no tearing between slices
- - Diff-based cell updates + run-length batching of gpu.set calls
- - Sparse world (only edits are stored) + heightmap shading
- - Exact DDA block picking, sliding collision, held-key movement
- - Textures: downloaded BMPs, or procedural fallback if no internet card
- - Music: Computronics sound card (generative chiptune), optional
- Controls
- W A S D move LShift sprint Space jump
- Arrows look Mouse drag look Scroll switch block
- E / B break / place 1 / 2 select block
- - / = lower / raise render quality
- M music on/off R random respawn
- Q quit
- ]]
- local component = require("component")
- local computer = require("computer")
- local event = require("event")
- local keyboard = require("keyboard")
- local unicode = require("unicode")
- local fs = require("filesystem")
- if not component.isAvailable("gpu") then error("A graphics card is required") end
- local gpu = component.gpu
- local K = keyboard.keys
- local floor, abs, cos, sin = math.floor, math.abs, math.cos, math.sin
- local min, max, sqrt, ceil, random = math.min, math.max, math.sqrt, math.ceil, math.random
- local concat = table.concat
- ---------------------------------------------------------------------
- -- Config
- ---------------------------------------------------------------------
- local ASSET_DIR = "/home/minecraft/"
- local ASSETS = {
- {"loading.bmp", "https://files.catbox.moe/i5nj03.bmp"},
- {"grass.bmp", "https://files.catbox.moe/x7tf9n.bmp"},
- {"stone.bmp", "https://files.catbox.moe/zs25m6.bmp"},
- }
- -- cols/rows are clipped to what your GPU/screen tier supports.
- -- sx*sy = rays per character cell (2x4 would be one ray per braille dot).
- local PRESETS = {
- {name = "Fast", cols = 80, rows = 25, sx = 1, sy = 2},
- {name = "Balanced", cols = 80, rows = 25, sx = 2, sy = 2},
- {name = "Sharp", cols = 80, rows = 25, sx = 2, sy = 4},
- {name = "Ultra", cols = 160, rows = 50, sx = 2, sy = 2},
- }
- local quality = 2
- local VIEW_DIST = 48 -- max DDA steps per ray
- local FOV = 1.1
- local SLICE_IDLE = 0.25 -- CPU seconds per render slice when standing still
- local SLICE_MOVE = 0.08 -- shorter slices while moving => input polled more often
- local LOWCONTRAST = 10 -- cells flatter than this become a solid colour
- local GRAV, JUMP, WALK, SPRINT, TURN = -20.0, 7.5, 4.3, 7.0, 1.9
- ---------------------------------------------------------------------
- -- Helpers
- ---------------------------------------------------------------------
- local function clamp(v, a, b) if v < a then return a elseif v > b then return b end return v end
- local function pack(r, g, b) return floor(r + 0.5) * 65536 + floor(g + 0.5) * 256 + floor(b + 0.5) end
- ---------------------------------------------------------------------
- -- Textures
- ---------------------------------------------------------------------
- local function u16(s, o) local a, b = s:byte(o, o + 1) return a + b * 256 end
- local function u32(s, o) local a, b, c, d = s:byte(o, o + 3) return a + b * 256 + c * 65536 + d * 16777216 end
- -- Loads a 24/32-bit BMP, optionally subsampled so max(w,h) <= maxDim.
- local function loadBMP(path, maxDim)
- local f = io.open(path, "rb")
- if not f then return nil end
- local data = f:read("*a")
- f:close()
- if not data or #data < 54 or data:sub(1, 2) ~= "BM" then return nil end
- local off = u32(data, 11) + 1
- local w = u32(data, 19)
- local hraw = u32(data, 23)
- local bpp = u16(data, 29)
- local comp = u32(data, 31)
- local topDown = false
- if hraw >= 2147483648 then hraw = 4294967296 - hraw; topDown = true end
- local h = hraw
- if (bpp ~= 24 and bpp ~= 32) or (comp ~= 0 and comp ~= 3) or w < 1 or h < 1 then return nil end
- local bytes = bpp / 8
- local stride = floor((bpp * w + 31) / 32) * 4
- local step = max(1, ceil(max(w, h) / (maxDim or 128)))
- local ow, oh = floor((w + step - 1) / step), floor((h + step - 1) / step)
- local px = {}
- for oy = 0, oh - 1 do
- local y = oy * step -- top-down row in the image
- local row = topDown and y or (h - 1 - y) -- row index inside the file
- local base = off + row * stride
- for ox = 0, ow - 1 do
- local o = base + ox * step * bytes
- local b, g, r = data:byte(o, o + 2)
- px[oy * ow + ox + 1] = r * 65536 + g * 256 + b
- end
- end
- return {w = ow, h = oh, px = px}
- end
- local function genTexture(kind)
- local w, h, px = 16, 16, {}
- local seed = kind == 1 and 1234 or 4321
- local function rnd() seed = (seed * 1103515245 + 12345) % 2147483648 return seed / 2147483648 end
- for i = 1, w * h do
- local n = rnd()
- if kind == 1 then px[i] = pack(55 + n * 35, 135 + n * 60, 40 + n * 30)
- else local g = 105 + n * 55; px[i] = pack(g, g, g + 4) end
- end
- return {w = w, h = h, px = px}
- end
- -- 4 pre-shaded variants so rendering is a single table lookup:
- -- 1 lit top, 2 lit side, 3 shaded (something above), 4 highlighted
- local function makeVariants(tex)
- local v1, v2, v3, v4 = {}, {}, {}, {}
- for i = 1, tex.w * tex.h do
- local c = tex.px[i]
- local r, g, b = floor(c / 65536), floor(c / 256) % 256, c % 256
- v1[i] = c
- v2[i] = pack(r * 0.8, g * 0.8, b * 0.8)
- v3[i] = pack(r * 0.42, g * 0.42, b * 0.46)
- v4[i] = pack(r + (255 - r) * 0.45, g + (255 - g) * 0.45, b + (255 - b) * 0.2)
- end
- tex.v = {v1, v2, v3, v4}
- end
- local function download(url, path)
- if fs.exists(path) then return true end
- if not component.isAvailable("internet") then return false end
- local ok = pcall(function()
- local internet = require("internet")
- local f = assert(io.open(path, "wb"))
- local good, err = pcall(function()
- for chunk in internet.request(url) do f:write(chunk) end
- end)
- f:close()
- if not good then fs.remove(path) error(err) end
- end)
- return ok and fs.exists(path)
- end
- local textures = {}
- local BLOCK_NAMES = {"Grass", "Stone"}
- local selectedBlock = 1
- ---------------------------------------------------------------------
- -- World: sparse edits over an infinite-feeling flat base
- -- y<4 stone(2), y==4 grass(1), above = air
- ---------------------------------------------------------------------
- local MAP_W, MAP_H, MAP_D = 256, 64, 256
- local edits, colTop, topY = {}, {}, 4
- local base = {}
- for y = 0, MAP_H - 1 do base[y] = (y == 4 and 1) or (y < 4 and 2) or 0 end
- local function setBlock(x, y, z, b)
- local ck = z * 256 + x
- local key = y * 65536 + ck
- if b == base[y] then edits[key] = nil else edits[key] = b end
- if b > 0 then
- if y > (colTop[ck] or 4) then colTop[ck] = y end
- if y > topY then topY = y end
- elseif y == (colTop[ck] or 4) then
- local ny = y - 1
- while ny >= 0 and (edits[ny * 65536 + ck] or base[ny]) == 0 do ny = ny - 1 end
- colTop[ck] = ny
- end
- end
- local function collides(x, y, z)
- for ix = floor(x - 0.3), floor(x + 0.3) do
- for iz = floor(z - 0.3), floor(z + 0.3) do
- if ix < 0 or ix >= MAP_W or iz < 0 or iz >= MAP_D then return true end
- for iy = floor(y - 1.4), floor(y) do
- if iy >= 0 and iy < MAP_H and (edits[iy * 65536 + iz * 256 + ix] or base[iy]) > 0 then
- return true
- end
- end
- end
- end
- return false
- end
- ---------------------------------------------------------------------
- -- Player state
- ---------------------------------------------------------------------
- local camX, camY, camZ = 128, 6.5, 128
- local lookDir, pitch = 0, 0
- local velY, onGround = 0, false
- local held = {}
- local dirty, running = true, true
- local function respawn()
- camX, camZ, camY, velY = random(20, 230), random(20, 230), 15, 0
- dirty = true
- end
- local lastLook = -10
- local moveKeys = {K.w, K.a, K.s, K.d, K.left, K.right, K.up, K.down, K.space}
- local function isMoving()
- if computer.uptime() - lastLook < 0.3 or not onGround then return true end
- for i = 1, #moveKeys do if held[moveKeys[i]] then return true end end
- return false
- end
- local function physics(dt)
- dt = min(dt, 0.25)
- local n = max(1, ceil(dt / 0.025))
- local h = dt / n
- local cL, sL = cos(lookDir), sin(lookDir)
- local mx, mz = 0, 0
- if held[K.w] then mx, mz = mx + cL, mz + sL end
- if held[K.s] then mx, mz = mx - cL, mz - sL end
- if held[K.a] then mx, mz = mx + sL, mz - cL end
- if held[K.d] then mx, mz = mx - sL, mz + cL end
- local moving = (mx ~= 0 or mz ~= 0)
- if moving then
- local sp = (held[K.lshift] and SPRINT or WALK) / sqrt(mx * mx + mz * mz)
- mx, mz = mx * sp, mz * sp
- end
- local turn = (held[K.right] and 1 or 0) - (held[K.left] and 1 or 0)
- if turn ~= 0 then lookDir = lookDir + turn * TURN * dt; dirty = true end
- local tilt = (held[K.up] and 1 or 0) - (held[K.down] and 1 or 0)
- if tilt ~= 0 then pitch = clamp(pitch + tilt * TURN * 0.8 * dt, -1.4, 1.4); dirty = true end
- local wantJump = held[K.space]
- for _ = 1, n do
- if moving then
- local nx = camX + mx * h
- if not collides(nx, camY, camZ) then camX = nx; dirty = true end
- local nz = camZ + mz * h
- if not collides(camX, camY, nz) then camZ = nz; dirty = true end
- end
- if wantJump and onGround then velY = JUMP; onGround = false end
- velY = velY + GRAV * h
- local ny = camY + velY * h
- if collides(camX, ny, camZ) then
- if velY < 0 then onGround = true end
- velY = 0
- else
- camY = ny; onGround = false; dirty = true
- end
- end
- if camY < -20 then respawn() end
- end
- ---------------------------------------------------------------------
- -- Block picking (exact DDA). Returns hit cell + the empty cell before it.
- ---------------------------------------------------------------------
- local function interactRay(maxDist)
- local cL, sL, cP, sP = cos(lookDir), sin(lookDir), cos(pitch), sin(pitch)
- local rdx, rdy, rdz = cL * cP, sP, sL * cP
- local ax, ay, az = abs(rdx), abs(rdy), abs(rdz)
- if ax < 1e-9 then ax = 1e-9 end
- if ay < 1e-9 then ay = 1e-9 end
- if az < 1e-9 then az = 1e-9 end
- local dX, dY, dZ = 1 / ax, 1 / ay, 1 / az
- local sX, sY, sZ = rdx < 0 and -1 or 1, rdy < 0 and -1 or 1, rdz < 0 and -1 or 1
- local mX, mY, mZ = floor(camX), floor(camY), floor(camZ)
- local sdX = rdx < 0 and (camX - mX) * dX or (mX + 1 - camX) * dX
- local sdY = rdy < 0 and (camY - mY) * dY or (mY + 1 - camY) * dY
- local sdZ = rdz < 0 and (camZ - mZ) * dZ or (mZ + 1 - camZ) * dZ
- for _ = 1, 64 do
- local pX, pY, pZ = mX, mY, mZ
- local t
- if sdX < sdY then
- if sdX < sdZ then t = sdX; sdX = sdX + dX; mX = mX + sX
- else t = sdZ; sdZ = sdZ + dZ; mZ = mZ + sZ end
- elseif sdY < sdZ then t = sdY; sdY = sdY + dY; mY = mY + sY
- else t = sdZ; sdZ = sdZ + dZ; mZ = mZ + sZ end
- if t > maxDist then return nil end
- 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
- if (edits[mY * 65536 + mZ * 256 + mX] or base[mY]) > 0 then return mX, mY, mZ, pX, pY, pZ end
- end
- end
- local function breakBlock()
- local hx, hy, hz = interactRay(8)
- if hx then setBlock(hx, hy, hz, 0); dirty = true end
- end
- local function placeBlock()
- local hx, hy, hz, px, py, pz = interactRay(8)
- if not hx then return end
- if px < 0 or px >= MAP_W or pz < 0 or pz >= MAP_D or py < 0 or py >= MAP_H then return end
- setBlock(px, py, pz, selectedBlock)
- if collides(camX, camY, camZ) then setBlock(px, py, pz, 0) return end -- don't entomb yourself
- dirty = true
- end
- ---------------------------------------------------------------------
- -- Colour handling for braille cells
- ---------------------------------------------------------------------
- local depth = gpu.getDepth()
- local rLev, gLev, bLev = {}, {}, {}
- for i = 0, 5 do rLev[i] = floor(i * 255 / 5) end
- for i = 0, 7 do gLev[i] = floor(i * 255 / 7) end
- for i = 0, 4 do bLev[i] = floor(i * 255 / 4) end
- -- snap to the tier-3 colour cube so equal colours really are equal on screen
- local function qCube(r, g, b)
- return rLev[floor(r * 5 / 255 + 0.5)] * 65536 + gLev[floor(g * 7 / 255 + 0.5)] * 256 + bLev[floor(b * 4 / 255 + 0.5)]
- end
- local function qPlain(r, g, b) return pack(r, g, b) end
- local quant = (depth == 8) and qCube or qPlain
- local Rc, Gc, Bc, Lc, solidMemo = {}, {}, {}, {}, {}
- local function cacheColor(c)
- local r, g, b = floor(c / 65536), floor(c / 256) % 256, c % 256
- Rc[c], Gc[c], Bc[c] = r, g, b
- Lc[c] = 0.299 * r + 0.587 * g + 0.114 * b
- end
- local dots, lums, samp, dmap = {0, 0, 0, 0, 0, 0, 0, 0}, {}, {}, {}
- -- dot k (1..8) = dy*2+dx+1 -> braille bit
- local bitOf = {1, 8, 2, 16, 4, 32, 64, 128}
- local chars = {[0] = " "}
- for b = 1, 255 do chars[b] = unicode.char(0x2800 + b) end
- -- Split the 8 dot colours into (bright -> ink, dark -> background)
- local function cluster()
- local c1 = dots[1]
- if c1 == dots[2] and c1 == dots[3] and c1 == dots[4] and c1 == dots[5]
- and c1 == dots[6] and c1 == dots[7] and c1 == dots[8] then
- local q = solidMemo[c1]
- if not q then
- if not Lc[c1] then cacheColor(c1) end
- q = quant(Rc[c1], Gc[c1], Bc[c1]); solidMemo[c1] = q
- end
- return q, q, 0
- end
- local sum = 0
- for k = 1, 8 do
- local c = dots[k]
- local l = Lc[c]
- if not l then cacheColor(c); l = Lc[c] end
- lums[k] = l; sum = sum + l
- end
- local mean = sum * 0.125
- local fr, fg, fb, nf, fl = 0, 0, 0, 0, 0
- local br, bg, bb, nb, bl = 0, 0, 0, 0, 0
- local bits = 0
- for k = 1, 8 do
- local c, l = dots[k], lums[k]
- if l > mean then
- bits = bits + bitOf[k]
- fr = fr + Rc[c]; fg = fg + Gc[c]; fb = fb + Bc[c]; nf = nf + 1; fl = fl + l
- else
- br = br + Rc[c]; bg = bg + Gc[c]; bb = bb + Bc[c]; nb = nb + 1; bl = bl + l
- end
- end
- if nf == 0 or nb == 0 or fl / nf - bl / nb < LOWCONTRAST then
- local q = quant((fr + br) * 0.125, (fg + bg) * 0.125, (fb + bb) * 0.125)
- return q, q, 0
- end
- local f = quant(fr / nf, fg / nf, fb / nf)
- local b = quant(br / nb, bg / nb, bb / nb)
- if f == b then return b, b, 0 end
- return f, b, bits
- end
- ---------------------------------------------------------------------
- -- Screen state + diffed, run-batched output
- ---------------------------------------------------------------------
- local cols, rows, gridRows, sx, sy, aspect = 80, 25, 24, 2, 2, 1
- local pfg, pbg, pbits = {}, {}, {}
- local curFg, curBg
- local runN, runX, runY, runFg, runBg, runInk, runBuf = 0, 0, 0, 0, 0, false, {}
- local baseSx, baseSy = 2, 2 -- preset sampling; passLod = this pass used the cheap sampling
- local passLod = false
- local backBuf -- VRAM back buffer index (nil = draw straight to screen)
- local minRow, maxRow = 1e9, -1 -- rows touched during the current pass (for partial bitblt)
- local function flush()
- if runN > 0 then
- if runInk and runFg ~= curFg then gpu.setForeground(runFg); curFg = runFg end
- if runBg ~= curBg then gpu.setBackground(runBg); curBg = runBg end
- gpu.set(runX, runY, concat(runBuf, "", 1, runN))
- runN = 0
- end
- end
- local function emitRow(row, fill)
- local y, rowBase = row + 1, row * cols
- for col = 0, cols - 1 do
- fill(row, col)
- local fg, bg, bits = cluster()
- local idx = rowBase + col + 1
- if pfg[idx] == fg and pbg[idx] == bg and pbits[idx] == bits then
- flush()
- else
- pfg[idx], pbg[idx], pbits[idx] = fg, bg, bits
- if row < minRow then minRow = row end
- if row > maxRow then maxRow = row end
- -- blank cells can ride along in any run with the same background
- if runN > 0 and runBg == bg and (bits == 0 or not runInk or runFg == fg) then
- runN = runN + 1; runBuf[runN] = chars[bits]
- if bits ~= 0 and not runInk then runFg = fg; runInk = true end
- else
- flush()
- runN, runX, runY, runFg, runBg, runInk = 1, col + 1, y, fg, bg, (bits ~= 0)
- runBuf[1] = chars[bits]
- end
- end
- end
- flush()
- end
- local function setColors(fg, bg)
- if fg ~= curFg then gpu.setForeground(fg); curFg = fg end
- if bg ~= curBg then gpu.setBackground(bg); curBg = bg end
- end
- -- Colour state is tracked per active buffer, so reset the trackers on every switch.
- local function activate(idx)
- gpu.setActiveBuffer(idx)
- curFg, curBg = nil, nil
- end
- local function allocBack()
- if not (gpu.allocateBuffer and gpu.setActiveBuffer and gpu.bitblt) then return nil end
- local ok, idx = pcall(gpu.allocateBuffer, cols, gridRows)
- if ok and idx then return idx end
- return nil -- not enough VRAM (or old OC): fall back to direct drawing
- end
- -- Clears the screen and the back buffer and forgets the diff cache.
- -- Leaves the back buffer (if any) as the active draw target.
- local function resetFrame()
- pfg, pbg, pbits = {}, {}, {}
- if backBuf then activate(0) end
- curFg, curBg = nil, nil
- setColors(0xFFFFFF, 0x000000)
- gpu.fill(1, 1, cols, rows, " ")
- if backBuf then
- activate(backBuf)
- setColors(0xFFFFFF, 0x000000)
- gpu.fill(1, 1, cols, gridRows, " ")
- end
- end
- -- Copies only the rows that changed this pass from VRAM to the screen in one call.
- local function presentFrame()
- if backBuf and maxRow >= minRow then
- gpu.bitblt(0, 1, minRow + 1, cols, maxRow - minRow + 1, backBuf, 1, minRow + 1)
- end
- minRow, maxRow = 1e9, -1
- end
- -- Draw text directly on the real screen (HUD), then go back to the back buffer.
- local function screenDraw(fn)
- if backBuf then activate(0) end
- fn()
- if backBuf then activate(backBuf) end
- end
- local function setSampling(a, b)
- sx, sy = a, b
- for k = 1, 8 do
- local dx, dy = (k - 1) % 2, floor((k - 1) / 2)
- dmap[k] = floor(dy * sy / 4) * sx + floor(dx * sx / 2) + 1
- end
- end
- local function applyQuality()
- if backBuf then
- pcall(gpu.setActiveBuffer, 0)
- pcall(gpu.freeBuffer, backBuf)
- backBuf = nil
- end
- local p = PRESETS[quality]
- local maxW, maxH = gpu.maxResolution()
- cols, rows = min(p.cols, maxW), min(p.rows, maxH)
- gpu.setResolution(cols, rows)
- gridRows = rows - 1 -- last row = HUD
- baseSx, baseSy = p.sx, p.sy
- setSampling(baseSx, baseSy)
- aspect = 2 * gridRows / cols -- braille dots are square
- backBuf = allocBack()
- minRow, maxRow = 1e9, -1
- resetFrame()
- end
- ---------------------------------------------------------------------
- -- Raycaster (one ray -> one packed RGB colour)
- ---------------------------------------------------------------------
- local sCX, sCY, sCZ, fX, fY, fZ = 0, 0, 0, 0, 0, 0 -- per-pass camera snapshot
- local hlX, hlY, hlZ -- highlighted block
- local function castRay(rdx, rdy, rdz, skyc)
- local ax = rdx < 0 and -rdx or rdx
- local ay = rdy < 0 and -rdy or rdy
- local az = rdz < 0 and -rdz or rdz
- if ax < 1e-9 then ax = 1e-9 end
- if ay < 1e-9 then ay = 1e-9 end
- if az < 1e-9 then az = 1e-9 end
- local dX, dY, dZ = 1 / ax, 1 / ay, 1 / az
- local sX, sY, sZ = rdx < 0 and -1 or 1, rdy < 0 and -1 or 1, rdz < 0 and -1 or 1
- local mX, mY, mZ = fX, fY, fZ
- local sdX = rdx < 0 and (sCX - mX) * dX or (mX + 1 - sCX) * dX
- local sdY = rdy < 0 and (sCY - mY) * dY or (mY + 1 - sCY) * dY
- local sdZ = rdz < 0 and (sCZ - mZ) * dZ or (mZ + 1 - sCZ) * dZ
- local up, top = rdy > 0, topY
- local ed, bs, ct = edits, base, colTop
- for _ = 1, VIEW_DIST do
- local t, side
- if sdX < sdY then
- if sdX < sdZ then t = sdX; sdX = sdX + dX; mX = mX + sX; side = 0
- else t = sdZ; sdZ = sdZ + dZ; mZ = mZ + sZ; side = 2 end
- elseif sdY < sdZ then t = sdY; sdY = sdY + dY; mY = mY + sY; side = 1
- else t = sdZ; sdZ = sdZ + dZ; mZ = mZ + sZ; side = 2 end
- 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
- if up and mY > top then return skyc end -- nothing can be above the tallest block
- local b = ed[mY * 65536 + mZ * 256 + mX] or bs[mY]
- if b > 0 then
- local tex = textures[b]
- local tw, th = tex.w, tex.h
- local hx, hy, hz = sCX + t * rdx, sCY + t * rdy, sCZ + t * rdz
- local u, v
- if side == 0 then u, v = hz - floor(hz), hy - floor(hy)
- elseif side == 1 then u, v = hx - floor(hx), hz - floor(hz)
- else u, v = hx - floor(hx), hy - floor(hy) end
- local tx = floor(u * tw); if tx >= tw then tx = tw - 1 end
- local ty = floor((1 - v) * th); if ty >= th then ty = th - 1 end
- local var
- if mX == hlX and mY == hlY and mZ == hlZ then var = 4
- elseif (ct[mZ * 256 + mX] or 4) >= mY + 2 then var = 3 -- covered from above
- elseif side ~= 1 then var = 2
- else var = 1 end
- return tex.v[var][ty * tw + tx + 1]
- end
- end
- return skyc
- end
- local function skyFor(ry)
- local t = clamp(ry * 1.2, 0, 1)
- return pack(170 + (60 - 170) * t, 220 + (140 - 220) * t, 255 + (235 - 255) * t)
- end
- local colAx, colAz, rowBx, rowBz, rowRy, rowSky = {}, {}, {}, {}, {}, {}
- local passActive, passRow, passStart = false, 0, 0
- local fps = 0
- local function beginPass()
- -- while moving, cast fewer rays per cell (1x2); refine at full quality once you stop
- passLod = (baseSx > 1 or baseSy > 2) and isMoving()
- if passLod then setSampling(1, 2) else setSampling(baseSx, baseSy) end
- sCX, sCY, sCZ = camX, camY, camZ
- fX, fY, fZ = floor(sCX), floor(sCY), floor(sCZ)
- local cL, sL, cP, sP = cos(lookDir), sin(lookDir), cos(pitch), sin(pitch)
- hlX, hlY, hlZ = interactRay(8)
- local scols, srows = cols * sx, gridRows * sy
- for sc = 0, scols - 1 do
- local vX = ((sc + 0.5) / scols - 0.5) * FOV
- colAx[sc] = cL * cP - vX * sL
- colAz[sc] = sL * cP + vX * cL
- end
- for sr = 0, srows - 1 do
- local vY = (0.5 - (sr + 0.5) / srows) * FOV * aspect
- rowBx[sr] = vY * cL * sP
- rowBz[sr] = vY * sL * sP
- local ry = sP + vY * cP
- rowRy[sr] = ry
- rowSky[sr] = skyFor(ry)
- end
- passRow, passActive, passStart = 0, true, computer.uptime()
- minRow, maxRow = 1e9, -1
- end
- local function gameCell(row, col)
- local n, sr0, sc0 = 0, row * sy, col * sx
- for j = 0, sy - 1 do
- local sr = sr0 + j
- local bx, bz, ry, sk = rowBx[sr], rowBz[sr], rowRy[sr], rowSky[sr]
- for i = 0, sx - 1 do
- local sc = sc0 + i
- n = n + 1
- samp[n] = castRay(colAx[sc] - bx, ry, colAz[sc] - bz, sk)
- end
- end
- for k = 1, 8 do dots[k] = samp[dmap[k]] end
- end
- local function drawHUD()
- local p = PRESETS[quality]
- local s = string.format(" %s | Q%d %s | %.1f fps | %s | %d %d %d",
- BLOCK_NAMES[selectedBlock], quality, p.name, fps, backBuf and "VRAM" or "direct",
- floor(camX), floor(camY), floor(camZ))
- s = s:sub(1, cols)
- s = s .. string.rep(" ", cols - #s)
- screenDraw(function()
- setColors(0xFFFFFF, 0x000000)
- gpu.set(1, rows, s)
- -- crosshair on the real screen (the blit overwrites it each pass, so no cache issues)
- local cx, cy = floor(cols / 2), floor(gridRows / 2)
- setColors(0xFFFFFF, pbg[cy * cols + cx + 1] or 0x000000)
- gpu.set(cx + 1, cy + 1, "+")
- end)
- end
- -- Renders rows until the CPU-time budget is used; returns true when the pass is complete.
- local function renderSlice(budget)
- local t0 = os.clock()
- while passRow < gridRows do
- emitRow(passRow, gameCell)
- passRow = passRow + 1
- if os.clock() - t0 > budget then break end
- end
- if passRow >= gridRows then passActive = false return true end
- return false
- end
- ---------------------------------------------------------------------
- -- Music (Computronics sound card, best effort)
- ---------------------------------------------------------------------
- local sound = component.isAvailable("sound") and component.sound or nil
- local musicOn = sound ~= nil
- local nextMusic = 0
- local NOTE_MS = 280
- local melody = {261.63, 293.66, 329.63, 392.00, 440.00, 523.25, 587.33, 659.25}
- local bassNotes = {130.81, 98.00, 110.00, 87.31}
- local mIdx, bIdx = 3, 1
- local function musicInit()
- if not sound then return end
- pcall(function() sound.setTotalVolume(0.6) end)
- pcall(function() sound.setWave(1, sound.modes.sine) end)
- pcall(function() sound.setWave(2, sound.modes.triangle) end)
- pcall(function() sound.setVolume(1, 0.35) end)
- pcall(function() sound.setVolume(2, 0.45) end)
- end
- local function musicStop()
- if not sound then return end
- pcall(function() sound.close(1); sound.close(2); sound.clear() end)
- end
- local function musicTick(now)
- if not musicOn or not sound or now < nextMusic then return end
- local steps = 8
- pcall(function()
- for step = 1, steps do
- if step % 4 == 1 then
- sound.setFrequency(2, bassNotes[bIdx]); sound.open(2)
- bIdx = bIdx % #bassNotes + 1
- end
- if random() > 0.2 then
- mIdx = clamp(mIdx + random(-2, 2), 1, #melody)
- sound.setFrequency(1, melody[mIdx]); sound.open(1)
- sound.delay(NOTE_MS - 50); sound.close(1); sound.delay(50)
- else
- sound.delay(NOTE_MS)
- end
- if step % 4 == 0 then sound.close(2) end
- end
- sound.process()
- end)
- nextMusic = now + steps * NOTE_MS / 1000 - 0.1
- end
- ---------------------------------------------------------------------
- -- Input (event listeners keep working even while the renderer is yielding)
- ---------------------------------------------------------------------
- local lastMX, lastMY
- local function onKeyDown(_, _, _, code)
- if held[code] then return end -- ignore OS key-repeat for one-shot actions
- held[code] = true
- if code == K.q then running = false
- elseif code == K["1"] then selectedBlock = 1; dirty = true
- elseif code == K["2"] then selectedBlock = 2; dirty = true
- elseif code == K.r then respawn()
- elseif code == K.e then breakBlock()
- elseif code == K.b then placeBlock()
- elseif code == K.m then
- musicOn = sound ~= nil and not musicOn
- if not musicOn then musicStop() else nextMusic = 0 end
- elseif code == K.minus or code == K.equals then
- local nq = clamp(quality + (code == K.equals and 1 or -1), 1, #PRESETS)
- if nq ~= quality then
- quality = nq
- passActive = false
- applyQuality()
- dirty = true
- end
- end
- end
- local function onKeyUp(_, _, _, code) held[code] = nil end
- local function onTouch(_, _, x, y) lastMX, lastMY = x, y end
- local function onDrag(_, _, x, y)
- lastLook = computer.uptime()
- if lastMX then
- lookDir = lookDir + (x - lastMX) * 0.05
- pitch = clamp(pitch + (lastMY - y) * 0.07, -1.4, 1.4)
- dirty = true
- end
- lastMX, lastMY = x, y
- end
- local function onDrop() lastMX, lastMY = nil, nil end
- local function onScroll(_, _, _, _, dir)
- selectedBlock = ((selectedBlock - 1 + (dir > 0 and -1 or 1)) % #BLOCK_NAMES) + 1
- dirty = true
- end
- local listeners = {
- key_down = onKeyDown, key_up = onKeyUp, touch = onTouch,
- drag = onDrag, drop = onDrop, scroll = onScroll,
- }
- ---------------------------------------------------------------------
- -- Main
- ---------------------------------------------------------------------
- -- Yield once, then drain EVERY queued signal. (os.sleep(0) only consumes one signal per
- -- tick, so key-repeat/drag floods backed up and key_up arrived late => "sticky" controls.)
- local function pump(timeout)
- local name = event.pull(timeout) -- listeners fire for each signal pulled
- local n = 0
- while name and n < 300 do
- name = event.pull(0)
- n = n + 1
- end
- end
- local function showLoading(img)
- applyQuality()
- if img then
- local function fill(row, col)
- for k = 1, 8 do
- local dx, dy = (k - 1) % 2, floor((k - 1) / 2)
- local u = (col + (dx + 0.5) / 2) / cols
- local v = (row + (dy + 0.5) / 4) / gridRows
- dots[k] = img.px[floor(v * img.h) * img.w + floor(u * img.w) + 1]
- end
- end
- for row = 0, gridRows - 1 do emitRow(row, fill) end
- presentFrame()
- end
- local help = " WASD move | Arrows/drag look | Space jump | E break | B place | 1/2 block | -/= quality | M music | Q quit - press a key"
- screenDraw(function()
- setColors(0xFFFFFF, 0x000000)
- gpu.set(1, rows, help:sub(1, cols) .. string.rep(" ", max(0, cols - #help)))
- end)
- event.pull("key_down")
- end
- local function main()
- if not fs.exists(ASSET_DIR) then fs.makeDirectory(ASSET_DIR) end
- for _, a in ipairs(ASSETS) do
- if not fs.exists(ASSET_DIR .. a[1]) then
- print("Fetching " .. a[1] .. " ...")
- if not download(a[2], ASSET_DIR .. a[1]) then print(" (unavailable, using fallback)") end
- end
- end
- local function try(name, maxDim)
- local ok, res = pcall(loadBMP, ASSET_DIR .. name, maxDim)
- return ok and res or nil
- end
- textures[1] = try("grass.bmp", 128) or genTexture(1)
- textures[2] = try("stone.bmp", 128) or genTexture(2)
- makeVariants(textures[1]); makeVariants(textures[2])
- local loadingImg = try("loading.bmp", 320)
- musicInit()
- showLoading(loadingImg)
- resetFrame()
- for name, fn in pairs(listeners) do event.listen(name, fn) end
- local last = computer.uptime()
- dirty = true
- while running do
- local now = computer.uptime()
- physics(now - last)
- last = now
- musicTick(now)
- if dirty and not passActive then dirty = false; beginPass() end
- if passActive then
- if renderSlice(isMoving() and SLICE_MOVE or SLICE_IDLE) then
- local dur = max(computer.uptime() - passStart, 0.05)
- fps = fps == 0 and 1 / dur or (fps * 0.7 + 0.3 / dur)
- presentFrame()
- drawHUD()
- if passLod and not isMoving() then dirty = true end -- refine at full quality
- end
- pump(0)
- else
- pump(0.05)
- end
- end
- end
- local oW, oH = gpu.getResolution()
- local ok, err = pcall(main)
- for name, fn in pairs(listeners) do pcall(event.ignore, name, fn) end
- musicStop()
- pcall(function() gpu.setActiveBuffer(0); gpu.freeAllBuffers() end)
- gpu.setResolution(oW, oH)
- gpu.setBackground(0x000000)
- gpu.setForeground(0xFFFFFF)
- gpu.fill(1, 1, oW, oH, " ")
- pcall(function() require("term").setCursor(1, 1) end)
- if not ok and not tostring(err):find("interrupted") then error(err, 0) end
Add Comment
Please, Sign In to add comment