SHOW:
|
|
- or go back to the newest paste.
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 | - | backbuffer[entity.y + ypos - 1][entity.x + xpos - 1] |
79 | + | --If the YPos doesn't exist, no need to loop through the rest of X! |
80 | - | = image[y][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 | - | error("not implemented yet...") |
254 | + | colour:?number = the colour to draw the rectangle (default red) |
255 | Returns:nil | |
256 | ]]-- | |
257 | local function drawBounds(entity, colour) | |
258 | - | type. Bases collision on a pixel occupancy approach. |
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 | - | error("not implemented yet..") |
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 | - | file:close() |
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 |