nonogamer9

Super Mario FX Clone For Craft OS-PC (On My Mode7 Engine!)

Apr 25th, 2025 (edited)
409
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 17.88 KB | Software | 0 0
  1. local sfxBuffer = {}
  2.  
  3. local function playSFX(filename, inputSampleRate, isUnsigned, volume)
  4.   local file = fs.open(filename, "rb")
  5.   if not file then return end
  6.   local samples, sum, count = {}, 0, 0
  7.   while true do
  8.     local byte = file.read()
  9.     if not byte then break end
  10.     if isUnsigned then byte = byte - 128 end
  11.     samples[#samples+1] = byte
  12.     sum = sum + byte
  13.     count = count + 1
  14.   end
  15.   file.close()
  16.   if #samples == 0 then return end
  17.   local avg = sum / count
  18.   for i=1,#samples do samples[i] = samples[i] - avg end
  19.   local outputSampleRate = 48000
  20.   if inputSampleRate ~= outputSampleRate then
  21.     local resampled = {}
  22.     local ratio = inputSampleRate / outputSampleRate
  23.     for i = 1, math.floor(#samples / ratio) do
  24.       local src = (i - 1) * ratio + 1
  25.       local idx = math.floor(src)
  26.       local frac = src - idx
  27.       local s1 = samples[idx] or 0
  28.       local s2 = samples[idx + 1] or s1
  29.       resampled[i] = s1 + (s2 - s1) * frac
  30.     end
  31.     samples = resampled
  32.   end
  33.   sfxSamples = samples
  34.   sfxPos = 1
  35. end
  36.  
  37.  
  38. local function playPCM(filename, inputSampleRate, isUnsigned, volume)
  39.   local speaker = peripheral.find("speaker")
  40.   if not speaker then return end
  41.   local file = fs.open(filename, "rb")
  42.   if not file then return end
  43.   local samples, sum, count = {}, 0, 0
  44.   while true do
  45.     local byte = file.read()
  46.     if not byte then break end
  47.     if isUnsigned then byte = byte - 128 end
  48.     samples[#samples+1] = byte
  49.     sum = sum + byte
  50.     count = count + 1
  51.   end
  52.   file.close()
  53.   if #samples == 0 then return end
  54.   local avg = sum / count
  55.   for i=1,#samples do samples[i] = samples[i] - avg end
  56.   local outputSampleRate = 48000
  57.   if inputSampleRate ~= outputSampleRate then
  58.     local resampled = {}
  59.     local ratio = inputSampleRate / outputSampleRate
  60.     for i = 1, math.floor(#samples / ratio) do
  61.       local src = (i - 1) * ratio + 1
  62.       local idx = math.floor(src)
  63.       local frac = src - idx
  64.       local s1 = samples[idx] or 0
  65.       local s2 = samples[idx + 1] or s1
  66.       resampled[i] = s1 + (s2 - s1) * frac
  67.     end
  68.     samples = resampled
  69.   end
  70.   local chunkSize = 128 * 1024
  71.   local totalSamples = #samples
  72.   local playedSamples = 0
  73.   local absSample = 1
  74.   while playedSamples < totalSamples do
  75.     local chunk = {}
  76.     for i = 1, chunkSize do
  77.       local idx = playedSamples + i
  78.       if idx > totalSamples then break end
  79.       local sample = math.floor(samples[idx] * volume + 0.5)
  80.       if sfxSamples and sfxPos and sfxPos <= #sfxSamples then
  81.         sample = sample + math.floor(sfxSamples[sfxPos])
  82.         if sample > 127 then sample = 127 end
  83.         if sample < -128 then sample = -128 end
  84.         sfxPos = sfxPos + 1
  85.         if sfxPos > #sfxSamples then
  86.           sfxSamples = nil
  87.           sfxPos = nil
  88.         end
  89.       end
  90.       chunk[i] = sample
  91.       absSample = absSample + 1
  92.     end
  93.     if #chunk == 0 then break end
  94.     while not speaker.playAudio(chunk) do
  95.       os.pullEvent("speaker_audio_empty")
  96.     end
  97.     playedSamples = playedSamples + #chunk
  98.   end
  99. end
  100.  
  101. local GROUND_BMP_URL = "https://files.catbox.moe/w9tlsn.bmp"
  102. local GROUND_BMP_FILE = "ground.bmp"
  103. local SPRITE_BMP_URL = "https://files.catbox.moe/fzau50.bmp"
  104. local SPRITE_BMP_FILE = "sprite.bmp"
  105. local FONT_BMP_URL = "https://files.catbox.moe/zkdsi9.bmp"
  106. local FONT_BMP_FILE = "font.bmp"
  107. local PCM_URL = "https://files.catbox.moe/kak23b.raw"
  108. local PCM_FILE = "music.raw"
  109. local SFX_URL = "https://files.catbox.moe/56o55a.raw"
  110. local SFX_FILE = "jump.raw"
  111. local LOADING_BMP_URL = "https://files.catbox.moe/l8tnog.bmp"
  112. local LOADING_BMP_FILE = "loading.bmp"
  113. local TRANSPARENT_COLOR = { r = 34, g = 177, b = 76 }
  114. local FONT_CELL_W, FONT_CELL_H = 20, 20
  115.  
  116. local function download(url, file)
  117.   if fs.exists(file) then return end
  118.   local h = http.get(url, nil, true)
  119.   assert(h, "Download failed: " .. url)
  120.   local f = fs.open(file, "wb")
  121.   f.write(h.readAll())
  122.   f.close()
  123.   h.close()
  124. end
  125.  
  126. local function readBMP(filename)
  127.   local file = fs.open(filename, "rb")
  128.   if not file then return nil, "Failed to open file" end
  129.   local header = file.read(54)
  130.   local width = string.byte(header, 19) + string.byte(header, 20) * 256
  131.   local height = string.byte(header, 23) + string.byte(header, 24) * 256
  132.   local pixels = {}
  133.   for y = height, 1, -1 do
  134.     pixels[y] = {}
  135.     for x = 1, width do
  136.       local b = file.read(1)
  137.       local g = file.read(1)
  138.       local r = file.read(1)
  139.       pixels[y][x] = {r = string.byte(r), g = string.byte(g), b = string.byte(b)}
  140.     end
  141.     if y % 10 == 0 then os.queueEvent("yield") os.pullEvent("yield") end
  142.   end
  143.   file.close()
  144.   return {width = width, height = height, pixels = pixels}
  145. end
  146.  
  147. local function mapColor256(r, g, b)
  148.   local cubeSize = 6
  149.   local rIndex = math.floor(r / (255 / (cubeSize - 1)))
  150.   local gIndex = math.floor(g / (255 / (cubeSize - 1)))
  151.   local bIndex = math.floor(b / (255 / (cubeSize - 1)))
  152.   return 16 + rIndex * cubeSize * cubeSize + gIndex * cubeSize + bIndex
  153. end
  154.  
  155. local function set256Palette()
  156.   local cubeSize = 6
  157.   for i = 16, 231 do
  158.     local index = i - 16
  159.     local rIndex = math.floor(index / (cubeSize * cubeSize))
  160.     local gIndex = math.floor((index % (cubeSize * cubeSize)) / cubeSize)
  161.     local bIndex = index % cubeSize
  162.     local r = math.floor(rIndex * 255 / (cubeSize - 1))
  163.     local g = math.floor(gIndex * 255 / (cubeSize - 1))
  164.     local b = math.floor(bIndex * 255 / (cubeSize - 1))
  165.     term.setPaletteColor(i, r / 255, g / 255, b / 255)
  166.   end
  167.   for i = 232, 255 do
  168.     local gray = math.floor((i - 232) * 255 / 23)
  169.     term.setPaletteColor(i, gray / 255, gray / 255, gray / 255)
  170.   end
  171. end
  172.  
  173. local function savePalette()
  174.   local t = {}
  175.   for i = 0, 255 do t[i] = {term.getPaletteColor(i)} end
  176.   return t
  177. end
  178. local function restorePalette(t)
  179.   for i = 0, 255 do term.setPaletteColor(i, table.unpack(t[i])) end
  180. end
  181.  
  182. local function makeBuffer(width, height)
  183.   local buf = {}
  184.   for y = 0, height-1 do
  185.     buf[y] = {}
  186.     for x = 0, width-1 do buf[y][x] = 0 end
  187.   end
  188.   return buf
  189. end
  190. local function swapBuffers(a, b) return b, a end
  191. local function blitBuffer(buf, output)
  192.   for y = 0, #buf do
  193.     for x = 0, #(buf[y]) do
  194.       output.setPixel(x, y, buf[y][x])
  195.     end
  196.   end
  197. end
  198.  
  199. local function downscaleBMP(img)
  200.   local w, h = math.floor(img.width / 4), math.floor(img.height / 4)
  201.   local out = {width=w, height=h, pixels={}}
  202.   for y=1,h do
  203.     out.pixels[y] = {}
  204.     for x=1,w do
  205.       out.pixels[y][x] = img.pixels[(y-1)*4+1][(x-1)*4+1]
  206.     end
  207.   end
  208.   return out
  209. end
  210.  
  211. local function drawTextHalfRes(font, x, y, str)
  212.   for i=1,#str do
  213.     local c = string.byte(str,i)-32
  214.     if c>=0 and c<120 then
  215.       local sx = (c%15)*20
  216.       local sy = math.floor(c/15)*20
  217.       for dy=0,9 do
  218.         for dx=0,9 do
  219.           local p = font.pixels[sy+dy*2+1][sx+dx*2+1]
  220.           if not (p.r==TRANSPARENT_COLOR.r and p.g==TRANSPARENT_COLOR.g and p.b==TRANSPARENT_COLOR.b) then
  221.             term.setPixel(x+(i-1)*10+dx, y+dy, mapColor256(p.r,p.g,p.b))
  222.           end
  223.         end
  224.       end
  225.     end
  226.   end
  227. end
  228.  
  229. local function drawSprite(img, x, y)
  230.   for iy=1,img.height do
  231.     for ix=1,img.width do
  232.       local p = img.pixels[iy][ix]
  233.       if not (p.r==TRANSPARENT_COLOR.r and p.g==TRANSPARENT_COLOR.g and p.b==TRANSPARENT_COLOR.b) then
  234.         term.setPixel(x+ix-1, y+iy-1, mapColor256(p.r, p.g, p.b))
  235.       end
  236.     end
  237.   end
  238. end
  239.  
  240. local function drawBMPScaled(bmp, pxw, pxh)
  241.   local iw, ih = bmp.width, bmp.height
  242.   for y = 0, pxh-1 do
  243.     local srcY = math.floor(y * ih / pxh) + 1
  244.     for x = 0, pxw-1 do
  245.       local srcX = math.floor(x * iw / pxw) + 1
  246.       local p = bmp.pixels[srcY][srcX]
  247.       term.setPixel(x+1, y+1, mapColor256(p.r, p.g, p.b))
  248.     end
  249.   end
  250. end
  251.  
  252. local function drawLoadingText(font, pxw, pxh, text)
  253.   local x = pxw - (#text * 10) - 10
  254.   local y = pxh - 15
  255.   drawTextHalfRes(font, x, y, text)
  256. end
  257.  
  258. local function showLoadingScreen()
  259.   term.setGraphicsMode(2)
  260.   set256Palette()
  261.   local pxw, pxh = term.getSize(2)
  262.   download(FONT_BMP_URL, FONT_BMP_FILE)
  263.   local font = readBMP(FONT_BMP_FILE)
  264.   download(LOADING_BMP_URL, LOADING_BMP_FILE)
  265.   local loading = readBMP(LOADING_BMP_FILE)
  266.   drawBMPScaled(loading, pxw, pxh)
  267.   drawLoadingText(font, pxw, pxh, "Loading font...")
  268.   download(GROUND_BMP_URL, GROUND_BMP_FILE)
  269.   drawBMPScaled(loading, pxw, pxh)
  270.   drawLoadingText(font, pxw, pxh, "Loading ground...")
  271.   download(SPRITE_BMP_URL, SPRITE_BMP_FILE)
  272.   drawBMPScaled(loading, pxw, pxh)
  273.   drawLoadingText(font, pxw, pxh, "Loading sprite...")
  274.   download(PCM_URL, PCM_FILE)
  275.   drawBMPScaled(loading, pxw, pxh)
  276.   drawLoadingText(font, pxw, pxh, "Loading music...")
  277.   download(SFX_URL, SFX_FILE)
  278.   drawBMPScaled(loading, pxw, pxh)
  279.   drawLoadingText(font, pxw, pxh, "Loading sfx...")
  280.   local ground = readBMP(GROUND_BMP_FILE)
  281.   drawBMPScaled(loading, pxw, pxh)
  282.   drawLoadingText(font, pxw, pxh, "Preparing ground...")
  283.   local sprite_big = readBMP(SPRITE_BMP_FILE)
  284.   local sprite = downscaleBMP(sprite_big)
  285.   drawBMPScaled(loading, pxw, pxh)
  286.   drawLoadingText(font, pxw, pxh, "Preparing sprite...")
  287.   drawBMPScaled(loading, pxw, pxh)
  288.   drawLoadingText(font, pxw, pxh, "Press any key to continue")
  289.   os.pullEvent("key")
  290.   return font, ground, sprite
  291. end
  292.  
  293. local player = { x = 150, y = 150, z = 0, angle = 0, vy = 0 }
  294. local CAMERA_HEIGHT = 120
  295. local MOVE_SPEED = 4
  296. local JUMP_VEL = 5
  297. local GRAVITY = 0.35
  298.  
  299. local function mode7_floor_player()
  300.   return function(frame, sw, sh, iw, ih)
  301.     local cam_x = player.x
  302.     local cam_y = player.y
  303.     local cam_angle = player.angle
  304.     local cam_height = CAMERA_HEIGHT + player.z
  305.     local horizon = math.floor((sh*9) * 0.45)
  306.     local focal_length = 170
  307.     return {
  308.       a=1, b=0, c=0, d=1, x0=0, y0=0, u0=0, v0=0,
  309.       scanline=function(sy,a,b,c,d,frame) return a,b,c,d end,
  310.       floor=function(sx,sy)
  311.         if sy < horizon then return nil,nil end
  312.         local pxw = sw*6
  313.         local dx = sx - pxw/2
  314.         local dy = sy - horizon
  315.         local angle = cam_angle
  316.         local distance = cam_height * focal_length / (dy+1)
  317.         local world_x = cam_x + math.cos(angle)*distance + math.sin(angle)*dx*distance/focal_length
  318.         local world_y = cam_y + math.sin(angle)*distance - math.cos(angle)*dx*distance/focal_length
  319.         local tx = ((math.floor(world_x)-1) % iw) + 1
  320.         local ty = ((math.floor(world_y)-1) % ih) + 1
  321.         return tx, ty
  322.       end,
  323.       horizon = horizon,
  324.       cam_x = cam_x, cam_y = cam_y, cam_angle = cam_angle, cam_height = cam_height
  325.     }
  326.   end
  327. end
  328.  
  329. local function displayBMPMode7Affine(image, output, paramFunc, animate, duration, afterFrame)
  330.   local screenWidth, screenHeight = output.getSize()
  331.   local pxw, pxh = screenWidth * 6, screenHeight * 9
  332.   local imageWidth, imageHeight = image.width, image.height
  333.   local originalPalette = savePalette()
  334.   local originalGraphicsMode = term.getGraphicsMode()
  335.   local originalBackgroundColor = term.getBackgroundColor()
  336.   local originalTextColor = term.getTextColor()
  337.   term.setGraphicsMode(2)
  338.   set256Palette()
  339.   local bufA = makeBuffer(pxw, pxh)
  340.   local bufB = makeBuffer(pxw, pxh)
  341.   local front, back = bufA, bufB
  342.   local numBands = 4
  343.   local bands = {}
  344.   local bandHeight = math.floor(pxh / numBands)
  345.   for i = 1, numBands do
  346.     local y0 = (i-1)*bandHeight
  347.     local y1 = (i==numBands) and (pxh-1) or (i*bandHeight-1)
  348.  bands[i] = {y0=y0, y1=y1}
  349.   end
  350.   local frame = 0
  351.   local t0 = os.epoch("utc")
  352.   local tEnd = t0 + (duration or 8)*1000
  353.   while os.epoch("utc") < tEnd do
  354.     frame = frame + 1
  355.     local params = paramFunc(frame, screenWidth, screenHeight, imageWidth, imageHeight)
  356.     local a, b, c, d = params.a, params.b, params.c, params.d
  357.     local x0, y0 = params.x0, params.y0
  358.     local u0, v0 = params.u0, params.v0
  359.     local floorFunc = params.floor
  360.     local horizon = params.horizon or 0
  361.     for bandIdx = 1, #bands do
  362.       local band = bands[bandIdx]
  363.       for sy = band.y0, band.y1 do
  364.         for sx = 0, pxw-1 do
  365.           local color = 0
  366.           if floorFunc then
  367.             local tx, ty
  368.             if params.multilayer then
  369.               tx, ty = floorFunc(sx, sy, 1)
  370.             else
  371.               tx, ty = floorFunc(sx, sy)
  372.             end
  373.             if tx and ty and tx >= 1 and tx <= imageWidth and ty >= 1 and ty <= imageHeight then
  374.               local pixel = image.pixels[ty][tx]
  375.               color = mapColor256(pixel.r, pixel.g, pixel.b)
  376.             else
  377.               color = 0
  378.             end
  379.           else
  380.             local sa, sb, sc, sd = a, b, c, d
  381.             if params.scanline then
  382.               sa, sb, sc, sd = params.scanline(sy, a, b, c, d, frame)
  383.             end
  384.             local dx = sx - x0
  385.             local dy = sy - y0
  386.             local u = sa * dx + sb * dy + u0
  387.             local v = sc * dx + sd * dy + v0
  388.             local srcX = math.floor(u + 0.5)
  389.             local srcY = math.floor(v + 0.5)
  390.             if srcX >= 1 and srcX <= imageWidth and srcY >= 1 and srcY <= imageHeight then
  391.               local pixel = image.pixels[srcY][srcX]
  392.               color = mapColor256(pixel.r, pixel.g, pixel.b)
  393.             else
  394.               color = 0
  395.             end
  396.           end
  397.           back[sy][sx] = color
  398.         end
  399.         if params.multilayer then
  400.           local tx, ty = params.floor(band.y0, sy, 2)
  401.           if tx and ty and tx >= 1 and tx <= imageWidth and ty >= 1 and ty <= imageHeight then
  402.             for sx = 0, pxw-1 do
  403.               local pixel = image.pixels[ty][tx]
  404.               back[sy][sx] = math.max(back[sy][sx], mapColor256(pixel.r, pixel.g, pixel.b))
  405.             end
  406.           end
  407.         end
  408.         if sy % 10 == 0 then os.queueEvent("yield") os.pullEvent("yield") end
  409.       end
  410.     end
  411.     if params.starfield then params.starfield(back) end
  412.     front, back = swapBuffers(front, back)
  413.     blitBuffer(front, output)
  414.     if afterFrame then afterFrame(params, frame, pxw, pxh) end
  415.     os.queueEvent("yield")
  416.     os.pullEvent("yield")
  417.   end
  418.   term.setGraphicsMode(originalGraphicsMode)
  419.   restorePalette(originalPalette)
  420.   term.setBackgroundColor(originalBackgroundColor)
  421.   term.setTextColor(originalTextColor)
  422.   term.clear()
  423.   term.setCursorPos(1, 1)
  424. end
  425.  
  426. local keymap = {
  427.   [keys.w]="forward", [keys.s]="back", [keys.a]="left", [keys.d]="right",
  428.   [keys.left]="turnl", [keys.right]="turnr", [keys.space]="jump"
  429. }
  430.  
  431. local font, ground, sprite = showLoadingScreen()
  432. term.clear()
  433. local screenWidth, screenHeight = term.getSize()
  434. local pxw, pxh = screenWidth*6, screenHeight*9
  435. local running = true
  436. local lastFrameTime = os.epoch("utc")
  437. parallel.waitForAny(
  438.   function()
  439.     while running do
  440.       local event = {os.pullEventRaw()}
  441.       if event[1] == "key" then
  442.         local action = keymap[event[2]]
  443.         if action == "forward" then
  444.           player.x = player.x + math.cos(player.angle) * MOVE_SPEED
  445.           player.y = player.y + math.sin(player.angle) * MOVE_SPEED
  446.         elseif action == "back" then
  447.           player.x = player.x - math.cos(player.angle) * MOVE_SPEED
  448.           player.y = player.y - math.sin(player.angle) * MOVE_SPEED
  449.         elseif action == "left" then
  450.           player.x = player.x + math.cos(player.angle-math.pi/2) * MOVE_SPEED
  451.           player.y = player.y + math.sin(player.angle-math.pi/2) * MOVE_SPEED
  452.         elseif action == "right" then
  453.           player.x = player.x + math.cos(player.angle+math.pi/2) * MOVE_SPEED
  454.           player.y = player.y + math.sin(player.angle+math.pi/2) * MOVE_SPEED
  455.         elseif action == "turnl" then
  456.           player.angle = player.angle - 0.1
  457.         elseif action == "turnr" then
  458.           player.angle = player.angle + 0.1
  459.         elseif action == "jump" and player.z == 0 then
  460.           player.vy = JUMP_VEL
  461.           coroutine.resume(coroutine.create(function()
  462.             local speaker = peripheral.find("speaker")
  463.             if not speaker then return end
  464.             local file = fs.open(SFX_FILE, "rb")
  465.             if not file then return end
  466.             local samples = {}
  467.             while true do
  468.               local byte = file.read()
  469.               if not byte then break end
  470.               byte = byte - 128
  471.               samples[#samples+1] = byte
  472.             end
  473.             file.close()
  474.             local chunkSize = 512
  475.             local total = #samples
  476.             local played = 0
  477.             while played < total do
  478.               local chunk = {}
  479.               for i=1,chunkSize do
  480.                 local idx = played + i
  481.                 if idx > total then break end
  482.                 chunk[i] = samples[idx]
  483.               end
  484.               if #chunk == 0 then break end
  485.               while not speaker.playAudio(chunk) do
  486.                 os.pullEvent("speaker_audio_empty")
  487.               end
  488.               played = played + #chunk
  489.             end
  490.           end))
  491.         end
  492.       elseif event[1] == "terminate" then
  493.         running = false
  494.         return
  495.       end
  496.     end
  497.   end,
  498.   function()
  499.     displayBMPMode7Affine(
  500.       ground, term, mode7_floor_player(), true, 1e9,
  501.       function(params, frame, pxw, pxh)
  502.         local now = os.epoch("utc")
  503.         local dt = now - lastFrameTime
  504.         lastFrameTime = now
  505.         local fps = dt > 0 and math.floor(1000 / dt) or 0
  506.         drawTextHalfRes(font, 10, 10, "FPS: " .. fps)
  507.         local creditText = "SMFX Clone by nonogamer9"
  508.         local textWidth = #creditText * 10
  509.         local centerX = math.floor((pxw - textWidth) / 2)
  510.         drawTextHalfRes(font, centerX, pxh - 20, creditText)
  511.         local sprite_y = math.floor(pxh/2 - sprite.height/2 - player.z)
  512.         drawSprite(sprite, math.floor(pxw/2-sprite.width/2), sprite_y)
  513.         if player.z > 0 or player.vy > 0 then
  514.           player.z = player.z + player.vy
  515.           player.vy = player.vy - GRAVITY
  516.           if player.z < 0 then player.z = 0; player.vy = 0 end
  517.         end
  518.       end
  519.     )
  520.   end,
  521.   function()
  522.     while true do
  523.       playPCM(PCM_FILE, 32000, true, 1)
  524.     end
  525.   end
  526. )
  527.  
Add Comment
Please, Sign In to add comment