View difference between Paste ID: s4qQgw3K and LVP1mWJG
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