Advertisement
nitrogenfingers

gameutils

Feb 7th, 2013
420
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. --[[
  2.     GameUtil
  3.     An API for drawing sprites and animations made in NPaintPro
  4.     By NitrogenFingers
  5. ]]--
  6.  
  7.  
  8. --The back buffer. Initialized as nil
  9. local backbuffer = nil
  10. --The bounds of the terminal the back buffer displays to
  11. local tw,th = nil, nil
  12.  
  13. --[[Constructs a new buffer. This must be done before the buffer can written to.
  14.     Params: terminal:?table = The function table to draw to a screen. By default (nil) this refers
  15.             to the native terminal, but monitor displays can be passed through as well:
  16.             local leftMonitor = peripherals.wrap("left")
  17.             initializeBuffer(leftMonitor)
  18.     Returns:boolean = True if the buffer was successfully initialized; false otherwise
  19. ]]--
  20. function initializeBuffer(terminal)
  21.     if not terminal then terminal = term end
  22.     if not terminal.getSize then
  23.         error("Parameter cannot be used to initialize the backbuffer.")
  24.     end
  25.     if not terminal.isColour() then
  26.         error("Parameter does not represent an advanced computer.")
  27.     end
  28.    
  29.     tw,th = terminal.getSize()
  30.     backbuffer = { }
  31.     for y=1,th do
  32.         backbuffer[y] = { }
  33.     end
  34.     return true
  35. end
  36.  
  37. --[[Will clear the buffer and reset to nil, or to a colour if provided
  38.     Params: colour:?number = The colour to set the back buffer to
  39.     Returns:nil
  40. ]]--
  41. function clearBuffer(colour)
  42.     if not backbuffer then
  43.         error("Back buffer not yet initialized!")
  44.     end
  45.    
  46.     for y=1,#backbuffer do
  47.         backbuffer[y] = { }
  48.         if colour then
  49.             for x=1,tw do
  50.                 backbuffer[y][x] = colour
  51.             end
  52.         end
  53.     end
  54. end
  55.  
  56. --[[Draws the given entity to the back buffer
  57.     Params: entity:table = the entity to draw to the buffer
  58.     Returns:nil
  59. ]]--
  60. function writeToBuffer(entity)
  61.     if not backbuffer then
  62.         error("Back buffer not yet initialized!")
  63.     end
  64.    
  65.     local image = nil
  66.     if entity.type == "animation" then
  67.         image = entity.frames[entity.currentFrame]
  68.     else
  69.         image = entity.image
  70.     end
  71.    
  72.     for y=1,image.dimensions.height do
  73.         for x=1,image.dimensions.width do
  74.             if image[y][x] then
  75.                 local xpos,ypos = x,y
  76.                 if entity.mirror.x then xpos = image.dimensions.width - x + 1 end
  77.                 if entity.mirror.y then ypos = image.dimensions.height - y + 1 end
  78.                
  79.                 --If the YPos doesn't exist, no need to loop through the rest of X!
  80.                 --Don't you love optimization?
  81.                 if not backbuffer[entity.y + ypos - 1] then break end
  82.                
  83.                 backbuffer[entity.y + ypos - 1][entity.x + xpos - 1] = image[y][x]
  84.             end
  85.         end
  86.     end
  87. end
  88.  
  89. --[[Draws the contents of the buffer to the screen. This will not clear the screen or the buffer.
  90.     Params: terminal:table = the terminal to draw to
  91.     Returns:nil
  92. ]]--
  93. function drawBuffer(terminal)
  94.     if not backbuffer then
  95.         error("Back buffer not yet initialized!")
  96.     end
  97.     if not terminal then terminal = term end
  98.     if not terminal.setCursorPos or not terminal.setBackgroundColour or not terminal.write then
  99.         error("Parameter cannot be used to initialize the backbuffer.")
  100.     end
  101.     if not terminal.isColour() then
  102.         error("Parameter does not represent an advanced computer.")
  103.     end
  104.    
  105.     for y=1,math.min(#backbuffer, th) do
  106.         for x=1,tw do
  107.             if backbuffer[y][x] then
  108.                 terminal.setCursorPos(x,y)
  109.                 terminal.setBackgroundColour(backbuffer[y][x])
  110.                 terminal.write(" ")
  111.             end
  112.         end
  113.     end
  114. end
  115.  
  116. --[[Converts a hex digit into a colour value
  117.     Params: hex:?string = the hex digit to be converted
  118.     Returns:string A colour value corresponding to the hex, or nil if the character is invalid
  119. ]]--
  120. local function getColourOf(hex)
  121.     local value = tonumber(hex, 16)
  122.     if not value then return nil end
  123.     value = math.pow(2,value)
  124.     return value
  125. end
  126.  
  127. --[[Converts every pixel of one colour in a given sprite to another colour
  128.     Use for "reskinning". Uses OO function.
  129.     Params: self:sprite = the sprite to reskin
  130.             oldcol:number = the colour to replace
  131.             newcol:number = the new colour
  132.     Returns:nil
  133. ]]--
  134. local function repaintS(self, oldcol, newcol)
  135.     for y=1,self.image.bounds.height do
  136.         for x=1, self.image.bounds.width do
  137.             if self.image[y][x] == oldcol then
  138.                 self.image[y][x] = newcol
  139.             end
  140.         end
  141.     end
  142. end
  143.  
  144. --[[Converts every pixel of one colour in a given animation to another colour
  145.     Use for "reskinning". Uses OO function.
  146.     Params: self:animation = the animation to reskin
  147.             oldcol:number = the colour to replace
  148.             newcol:number = the new colour
  149.     Returns:nil
  150. ]]--
  151. local function repaintA(self, oldcol, newcol)
  152.     for f=1,#self.frames do
  153.         print(self.frames[f].bounds)
  154.         for y=1,self.frames[f].bounds.height do
  155.             for x=1, self.frames[f].bounds.width do
  156.                 if self.frames[f][y][x] == oldcol then
  157.                     self.frames[f][y][x] = newcol
  158.                 end
  159.             end
  160.         end
  161.     end
  162. end
  163.  
  164. --[[Prints the sprite on the screen
  165.     Params: self:sprite = the sprite to draw
  166.     Returns:nil
  167. ]]--
  168. local function drawS(self)
  169.     local image = self.image
  170.    
  171.     for y=1,image.dimensions.height do
  172.         for x=1,image.dimensions.width do
  173.             if image[y][x] then
  174.                 local xpos,ypos = x,y
  175.                 if self.mirror.x then xpos = image.dimensions.width - x + 1 end
  176.                 if self.mirror.y then ypos = image.dimensions.height - y + 1 end
  177.                
  178.                 term.setBackgroundColour(image[y][x])
  179.                 term.setCursorPos(self.x + xpos - 1, self.y + ypos - 1)
  180.                 term.write(" ")
  181.             end
  182.         end
  183.     end
  184. end
  185.  
  186. --[[Prints the current frame of the animation on screen
  187.     Params: self:anim = the animation to draw
  188.             frame:?number = the specific frame to draw (default self.currentFrame)
  189.     Returns:nil
  190. ]]--
  191. local function drawA(self, frame)
  192.     if not frame then frame = self.currentFrame end
  193.     local image = self.frames[frame]
  194.  
  195.     for y=1,image.dimensions.height do
  196.         for x=1,image.dimensions.width do
  197.             if image[y][x] then
  198.                 local xpos,ypos = x,y
  199.                 if self.mirror.x then xpos = image.dimensions.width - x + 1 end
  200.                 if self.mirror.y then ypos = image.dimensions.height - y + 1 end
  201.                
  202.                 term.setBackgroundColour(image[y][x])
  203.                 term.setCursorPos(self.x + xpos - 1, self.y + ypos - 1)
  204.                 term.write(" ")
  205.             end
  206.         end
  207.     end
  208. end
  209.  
  210. --[[Checks the animation timer provided to see whether or not the animation needs to be updated.
  211.     If so, it makes the necessary change.
  212.     Params: self:animation = the animation to be updated
  213.             timerID:number = the ID of the most recent timer event
  214.     Returns:bool = true if the animation was update; false otherwise
  215. ]]--
  216. local function updateA(self, timerID)
  217.     if self.timerID and timerID and self.timerID == timerID then
  218.         self.currentFrame = self.currentFrame + 1
  219.         if self.currentFrame > self.upperBound then
  220.             self.currentFrame = self.lowerBound
  221.         end
  222.         return true
  223.     else
  224.         return false
  225.     end
  226. end
  227.  
  228. --[[Moves immediately to the next frame in the sequence, as though an update had been called.
  229.     Params: self:animation = the animation to update
  230.     Returns:nil
  231. ]]--
  232. local function nextA(self)
  233.     self.currentFrame = self.currentFrame + 1
  234.     if self.currentFrame > self.upperBound then
  235.         self.currentFrame = self.lowerBound
  236.     end
  237. end
  238.  
  239. --[[Moves immediately to the previous frame in the sequence
  240.     Params: self:animation = the animation to update
  241.     Returns:nil
  242. ]]--
  243. local function previousA(self)
  244.     self.currentFrame = self.currentFrame - 1
  245.     if self.currentFrame < self.lowerBound then
  246.         self.currentFrame = self.upperBound
  247.     end
  248. end
  249.  
  250. --[[A simple debug function that displays the outline of the bounds
  251.     on a given shape. Useful when testing collision detection or other game
  252.     features.
  253.     Params: entity:table = the bounded entity to represent
  254.             colour:?number = the colour to draw the rectangle (default red)
  255.     Returns:nil
  256. ]]--
  257. local function drawBounds(entity, colour)
  258.     if not colour then colour = colours.red end
  259.     local image = nil
  260.     if entity.type == "animation" then image = entity.frames[entity.currentFrame]
  261.     else image = entity.image end
  262.    
  263.     term.setBackgroundColour(colour)
  264.    
  265.     corners = {
  266.         topleft = { x = entity.x + image.bounds.x - 1, y = entity.y + image.bounds.y - 1 };
  267.         topright = { x = entity.x + image.bounds.x + image.bounds.width - 2, y = entity.y + image.bounds.y - 1 };
  268.         botleft = { x = entity.x + image.bounds.x - 1, y = entity.y + image.bounds.y + image.bounds.height - 2 };
  269.         botright = { x = entity.x + image.bounds.x + image.bounds.width - 2, y = entity.y + image.bounds.y + image.bounds.height - 2 };
  270.     }
  271.    
  272.     term.setCursorPos(corners.topleft.x, corners.topleft.y)
  273.     term.write(" ")
  274.     term.setCursorPos(corners.topright.x, corners.topright.y)
  275.     term.write(" ")
  276.     term.setCursorPos(corners.botleft.x, corners.botleft.y)
  277.     term.write(" ")
  278.     term.setCursorPos(corners.botright.x, corners.botright.y)
  279.     term.write(" ")
  280. end
  281.  
  282. --[[Creates a bounding rectangle object. Used in drawing the bounds and the rCollidesWith methods
  283.     Params: self:table = the entity to create the rectangle
  284.     Returns:table = the left, right, top and bottom edges of the rectangle
  285. ]]--
  286. local function createRectangle(entity)
  287.     local image = nil
  288.     if entity.type == "animation" then
  289.         image = entity.frames[entity.currentFrame]
  290.     else
  291.         image = entity.image
  292.     end
  293.     --Note that the origin is always 1, so we subtract 1 for every absolute coordinate we have to test.
  294.     return {
  295.         left = entity.x + image.bounds.x - 1;
  296.         right = entity.x + image.bounds.x + image.bounds.width - 2;
  297.         top = entity.y + image.bounds.y - 1;
  298.         bottom = entity.y + image.bounds.y + image.bounds.height - 2;
  299.     }
  300. end
  301.  
  302. --[[Performs a rectangle collision with another given entity. Entity can be of sprite or animation
  303.     type (also true of the self). Bases collision using a least squared approach (rectangle precision).
  304.     Params: self:sprite,animation = the object in question of the testing
  305.             other:sprite,animation = the other object tested for collision
  306.     Returns:bool = true if bounding rectangle intersect is true; false otherwse
  307. ]]--
  308. local function rCollidesWith(self, other)
  309.     --First we construct the rectangles
  310.     local img1C, img2C = createRectangle(self), createRectangle(other)
  311.    
  312.     --We then determine the "relative position" , in terms of which is farther left or right
  313.     leftmost,rightmost,topmost,botmost = nil,nil,nil,nil
  314.     if img1C.left < img2C.left then
  315.         leftmost = img1C
  316.         rightmost = img2C
  317.     else
  318.         leftmost = img2C
  319.         rightmost = img1C
  320.     end
  321.     if img1C.top < img2C.top then
  322.         topmost = img1C
  323.         botmost = img2C
  324.     else
  325.         topmost = img2C
  326.         botmost = img1C
  327.     end
  328.    
  329.     --Then we determine the distance between the "extreme" edges-
  330.         --distance between leftmost/right edge and rightmost/left edge
  331.         --distance between topmost/bottom edge and bottommost/top edge
  332.     local xdist = rightmost.left - leftmost.right
  333.     local ydist = botmost.top - topmost.bottom
  334.    
  335.     --If both are negative, our rectangles intersect!
  336.     return xdist <= 0 and ydist <= 0
  337. end
  338.  
  339. --[[Performs a pixel collision test on another given entity. Either entity can be of sprite or animation
  340.     type. This is done coarsegrain-finegrain, we first find the intersection between the rectangles
  341.     (if there is one), and then test the space within that intersection for any intersecting pixels.
  342.     Params: self:sprite,animation = the object in question of the testing
  343.             other:sprite,animation = the other object being tested for collision
  344.     Returns:?number,?number: The X and Y position in which the collision occurred.
  345. ]]--
  346. local function pCollidesWith(self, other)
  347.     --Identically to rCollidesWith, we create our rectangles...
  348.     local img1C, img2C = createRectangle(self), createRectangle(other)
  349.     --We'll also need the images to compare pixels later
  350.     local img1, img2 = nil,nil
  351.     if self.type == "animation" then img1 = self.frames[self.currentFrame]
  352.     else img1 = self.image end
  353.     if other.type == "animation" then img2 = other.frames[other.currentFrame]
  354.     else img2 = other.image end
  355.    
  356.     --...then we position them...
  357.     leftmost,rightmost,topmost,botmost = nil,nil,nil,nil
  358.     --We also keep track of which is left and which is right- it doesn't matter in a rectangle
  359.     --collision but it does in a pixel collision.
  360.     img1T,img2T = {},{}
  361.    
  362.     if img1C.left < img2C.left then
  363.         leftmost = img1C
  364.         rightmost = img2C
  365.         img1T.left = true
  366.     else
  367.         leftmost = img2C
  368.         rightmost = img1C
  369.         img2T.left = true
  370.     end
  371.     if img1C.top < img2C.top then
  372.         topmost = img1C
  373.         botmost = img2C
  374.         img1T.top = true
  375.     else
  376.         topmost = img2C
  377.         botmost = img1C
  378.         img2T.top = true
  379.     end
  380.    
  381.     --...and we again find the distances between the extreme edges.
  382.     local xdist = rightmost.left - leftmost.right
  383.     local ydist = botmost.top - topmost.bottom
  384.    
  385.     --If these distances are > 0 then we stop- no need to go any farther.
  386.     if xdist > 0 or ydist > 0 then return false end
  387.    
  388.    
  389.     for x = rightmost.left, rightmost.left + math.abs(xdist) do
  390.         for y = botmost.top, botmost.top + math.abs(ydist) do
  391.             --We know a collision has occurred if a pixel is occupied by both images. We do this by
  392.             --first transforming the coordinates based on which rectangle is which, then testing if a
  393.             --pixel is at that point
  394.                 -- The leftmost and topmost takes the distance on x and y and removes the upper component
  395.                 -- The rightmost and bottommost, being the farther extremes, compare from 1 upwards
  396.             local testX,testY = 1,1
  397.             if img1T.left then testX = x - img1C.left + 1
  398.             else testX = x - img1C.left + 1 end
  399.             if img1T.top then testY = y - img1C.top + 1
  400.             else testY = y - img1C.top + 1 end
  401.            
  402.             local occupy1 = img1[testY + img1.bounds.y-1][testX + img1.bounds.x-1] ~= nil
  403.            
  404.             if img2T.left then testX = x - img2C.left + 1
  405.             else testX = x - img2C.left + 1 end
  406.             if img2T.top then testY = y - img2C.top + 1
  407.             else testY = y - img2C.top + 1 end
  408.            
  409.             local occupy2 = img2[testY + img2.bounds.y-1][testX + img2.bounds.x-1] ~= nil
  410.            
  411.             if occupy1 and occupy2 then return true end
  412.         end
  413.     end
  414.     --If the looop terminates without returning, then no pixels overlap
  415.     return false
  416. end
  417.  
  418. --[[
  419.     Sprites Fields:
  420. x:number = the x position of the sprite in the world
  421. y:number = the y position of the sprite in the world
  422. image:table = a table of the image. Indexed by height, a series of sub-tables, each entry being a pixel
  423.         at [y][x]. It also contains:
  424.     bounds:table =
  425.         x:number = the relative x position of the bounding rectangle
  426.         y:number = the relative y position of the bounding rectangle
  427.         width:number = the width of the bounding rectangle
  428.         height:number = the height of the bounding rectangle
  429.     dimensions:table =
  430.         width = the width of the entire image in pixels
  431.         height = the height of the entire image in pixels
  432.        
  433. mirror:table =
  434.     x:bool = whether or not the image is mirrored on the X axis
  435.     y:bool = whether or not the image is mirrored on the Y axis
  436. repaint:function = see repaintS (above)
  437. rCollidesWith:function = see rCollidesWith (above)
  438. pCollidesWith:function = see pCollidesWith (above)
  439. draw:function = see drawS (above)
  440. ]]--
  441.  
  442. --[[Loads a new sprite into a table, and returns it to the user.
  443.     Params: path:string = the absolute path to the desired sprite
  444.     x:number = the initial X position of the sprite
  445.     y:number = the initial Y position of the sprite
  446. ]]--
  447. function loadSprite(path, x, y)
  448.     local sprite = {
  449.         type = "sprite",
  450.         x = x,
  451.         y = y,
  452.         image = { },
  453.         mirror = { x = false, y = false }
  454.     }
  455.    
  456.     if fs.exists(path) then
  457.         local file = io.open(path, "r" )
  458.         local leftX, rightX = math.huge, 0
  459.         local topY, botY = nil,nil
  460.        
  461.         local lcount = 0
  462.         for line in file:lines() do
  463.             lcount = lcount+1
  464.             table.insert(sprite.image, {})
  465.             for i=1,#line do
  466.                 if string.sub(line, i, i) ~= " " then
  467.                     leftX = math.min(leftX, i)
  468.                     rightX = math.max(rightX, i)
  469.                     if not topY then topY = lcount end
  470.                     botY = lcount
  471.                 end
  472.                 sprite.image[#sprite.image][i] = getColourOf(string.sub(line,i,i))
  473.             end
  474.         end
  475.         file:close()
  476.        
  477.         sprite.image.bounds = {
  478.             x = leftX,
  479.             width = rightX - leftX + 1,
  480.             y = topY,
  481.             height = botY - topY + 1
  482.         }
  483.         sprite.image.dimensions = {
  484.             width = rightX,
  485.             height = botY
  486.         }
  487.        
  488.         sprite.x = sprite.x - leftX + 1
  489.         sprite.y = sprite.y - topY + 1
  490.        
  491.         sprite.repaint = repaintS
  492.         sprite.rCollidesWith = rCollidesWith
  493.         sprite.pCollidesWith = pCollidesWith
  494.         sprite.draw = drawS
  495.         return sprite
  496.     else
  497.         error(path.." not found!")
  498.     end
  499. end
  500.  
  501. --Animations contain
  502.     --Everything a sprite contains, but the image is a series of frames, not just one image
  503.     --An timerID that tracks the last animation
  504.     --An upper and lower bound on the active animation
  505.     --An update method that takes a timer event and updates the animation if necessary
  506.  
  507. --[[
  508.  
  509. ]]--
  510. function loadAnimation(path, x, y, currentFrame)
  511.     local anim = {
  512.         type = "animation",
  513.         x = x,
  514.         y = y,
  515.         frames = { },
  516.         mirror = { x = false, y = false },
  517.         currentFrame = currentFrame
  518.     }
  519.    
  520.     table.insert(anim.frames, { })
  521.     if fs.exists(path) then
  522.         local file = io.open(path, "r")
  523.         local leftX, rightX = math.huge, 0
  524.         local topY, botY = nil,nil
  525.        
  526.         local lcount = 0
  527.         for line in file:lines() do
  528.             lcount = lcount+1
  529.             local cFrame = #anim.frames
  530.             print("["..line.."]")
  531.             if line == "~" then
  532.                 print(leftX," ",rightX," ",topY," ",botY)
  533.                 anim.frames[cFrame].bounds = {
  534.                     x = leftX,
  535.                     y = topY,
  536.                     width = rightX - leftX + 1,
  537.                     height = botY - topY + 1
  538.                 }
  539.                 anim.frames[cFrame].dimensions = {
  540.                     width = rightX,
  541.                     height = botY
  542.                 }
  543.                 table.insert(anim.frames, { })
  544.                 leftX, rightX = math.huge, 0
  545.                 topY, botY = nil,nil
  546.                 lcount = 0
  547.             else
  548.                 table.insert(anim.frames[cFrame], {})
  549.                 for i=1,#line do
  550.                     if string.sub(line, i, i) ~= " " then
  551.                         leftX = math.min(leftX, i)
  552.                         rightX = math.max(rightX, i)
  553.                         if not topY then topY = lcount end
  554.                         botY = lcount
  555.                     end
  556.                     anim.frames[cFrame][#anim.frames[cFrame]] [i] = getColourOf(string.sub(line,i,i))
  557.                 end
  558.             end
  559.         end
  560.         file:close()
  561.         local cFrame = #anim.frames
  562.         anim.frames[cFrame].bounds = {
  563.             x = leftX,
  564.             y = topY,
  565.             width = rightX - leftX + 1,
  566.             height = botY - topY + 1
  567.         }
  568.        
  569.         if not currentFrame or type(currentFrame) ~= "number" or currentFrame < 1 or
  570.                 currentFrame > #anim.frames then
  571.             anim.currentFrame = 1
  572.         end
  573.        
  574.         print("continue")
  575.         os.pullEvent("key")
  576.    
  577.         anim.timerID = nil
  578.         anim.lowerBound = 1
  579.         anim.upperBound = #anim.frames
  580.         anim.updating = false
  581.    
  582.         anim.repaint = repaintA
  583.         anim.rCollidesWith = rCollidesWith
  584.         anim.pCollidesWith = pCollidesWith
  585.         anim.draw = drawA
  586.         anim.update = updateA
  587.         anim.next = nextA
  588.         anim.previous = previousA
  589.         return anim
  590.     else
  591.         error(path.." not found!")
  592.     end
  593. end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement