View difference between Paste ID: 2MMjiFjG and s4qQgw3K
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
				--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
--[[Moves the sprite or animation to the specified coordinates. This performs the auto-centering, so
419
	the user doesn't have to worry about adjusting for the bounds of the shape. Recommended for absolute
420
	positioning operations (as relative direct access to the X will have unexpected results!)
421
	Params: self:table = the animation or sprite to move
422
	x:number = the new x position
423
	y:number = the new y position
424
]]--
425
local function moveTo(self, x, y)
426
	local image = nil
427
	if self.type == "animation" then
428
		image = self.frames[self.currentFrame]
429
	else
430
		image = self.image
431
	end
432
	
433
	self.x = x - image.bounds.x + 1
434
	self.y = y - image.bounds.y + 1
435
end
436
437
--[[
438
	Sprites Fields:
439
x:number = the x position of the sprite in the world
440
y:number = the y position of the sprite in the world
441
image:table = a table of the image. Indexed by height, a series of sub-tables, each entry being a pixel
442
		at [y][x]. It also contains:
443
	bounds:table =
444
		x:number = the relative x position of the bounding rectangle
445
		y:number = the relative y position of the bounding rectangle
446
		width:number = the width of the bounding rectangle
447
		height:number = the height of the bounding rectangle
448
	dimensions:table =
449
		width = the width of the entire image in pixels
450
		height = the height of the entire image in pixels
451
		
452
mirror:table =
453
	x:bool = whether or not the image is mirrored on the X axis
454
	y:bool = whether or not the image is mirrored on the Y axis
455
repaint:function = see repaintS (above)
456
rCollidesWith:function = see rCollidesWith (above)
457
pCollidesWith:function = see pCollidesWith (above)
458
draw:function = see drawS (above)
459
]]--
460
461
--[[Loads a new sprite into a table, and returns it to the user.
462
	Params: path:string = the absolute path to the desired sprite
463
	x:number = the initial X position of the sprite
464
	y:number = the initial Y position of the sprite
465
]]--
466
function loadSprite(path, x, y)
467
	local sprite = { 
468
		type = "sprite",
469
		x = x,
470
		y = y,
471
		image = { },
472
		mirror = { x = false, y = false }
473
	}
474
	
475
	if fs.exists(path) then
476
		local file = io.open(path, "r" )
477
		local leftX, rightX = math.huge, 0
478
		local topY, botY = nil,nil
479
		
480
		local lcount = 0
481
		for line in file:lines() do
482
			lcount = lcount+1
483
			table.insert(sprite.image, {})
484
			for i=1,#line do
485
				if string.sub(line, i, i) ~= " " then
486
					leftX = math.min(leftX, i)
487
					rightX = math.max(rightX, i)
488
					if not topY then topY = lcount end
489
					botY = lcount
490
				end
491
				sprite.image[#sprite.image][i] = getColourOf(string.sub(line,i,i))
492
			end
493
		end
494
		file:close()
495
		
496
		sprite.image.bounds = {
497
			x = leftX,
498
			width = rightX - leftX + 1,
499
			y = topY,
500
			height = botY - topY + 1
501
		}
502
		sprite.image.dimensions = {
503
			width = rightX,
504
			height = botY
505
		}
506
		
507
		sprite.x = sprite.x - leftX + 1
508
		sprite.y = sprite.y - topY + 1
509
		
510
		sprite.repaint = repaintS
511
		sprite.rCollidesWith = rCollidesWith
512
		sprite.pCollidesWith = pCollidesWith
513
		sprite.draw = drawS
514
		sprite.moveTo = moveTo
515
		return sprite
516
	else
517
		error(path.." not found!")
518
	end
519
end
520
521
--Animations contain
522
	--Everything a sprite contains, but the image is a series of frames, not just one image
523
	--An timerID that tracks the last animation
524
	--An upper and lower bound on the active animation
525
	--An update method that takes a timer event and updates the animation if necessary
526
527
--[[
528
529
]]--
530-
			print("["..line.."]")
530+
531
	local anim = {
532-
				print(leftX," ",rightX," ",topY," ",botY)
532+
533
		x = x,
534
		y = y,
535
		frames = { },
536
		mirror = { x = false, y = false },
537
		currentFrame = currentFrame
538
	}
539
	
540
	table.insert(anim.frames, { })
541
	if fs.exists(path) then
542
		local file = io.open(path, "r")
543
		local leftX, rightX = math.huge, 0
544
		local topY, botY = nil,nil
545
		
546
		local lcount = 0
547
		for line in file:lines() do
548
			lcount = lcount+1
549
			local cFrame = #anim.frames
550
			if line == "~" then
551
				anim.frames[cFrame].bounds = {
552
					x = leftX,
553
					y = topY,
554
					width = rightX - leftX + 1,
555
					height = botY - topY + 1
556
				}
557
				anim.frames[cFrame].dimensions = {
558
					width = rightX,
559
					height = botY
560
				}
561
				table.insert(anim.frames, { })
562
				leftX, rightX = math.huge, 0
563
				topY, botY = nil,nil
564
				lcount = 0
565
			else
566
				table.insert(anim.frames[cFrame], {})
567
				for i=1,#line do
568
					if string.sub(line, i, i) ~= " " then
569
						leftX = math.min(leftX, i)
570
						rightX = math.max(rightX, i)
571
						if not topY then topY = lcount end
572
						botY = lcount
573
					end
574-
		print("continue")
574+
575-
		os.pullEvent("key")
575+
576
			end
577
		end
578
		file:close()
579
		local cFrame = #anim.frames
580
		anim.frames[cFrame].bounds = {
581
			x = leftX,
582
			y = topY,
583
			width = rightX - leftX + 1,
584
			height = botY - topY + 1
585
		}
586
		anim.frames[cFrame].dimensions = {
587
			width = rightX,
588
			height = botY
589
		}
590
		anim.x = anim.x - leftX + 1
591
		anim.y = anim.y - topY + 1
592
		
593
		if not currentFrame or type(currentFrame) ~= "number" or currentFrame < 1 or 
594
				currentFrame > #anim.frames then 
595
			anim.currentFrame = 1 
596
		end
597
	
598
		anim.timerID = nil
599
		anim.lowerBound = 1
600
		anim.upperBound = #anim.frames
601
		anim.updating = false
602
	
603
		anim.repaint = repaintA
604
		anim.rCollidesWith = rCollidesWith
605
		anim.pCollidesWith = pCollidesWith
606
		anim.draw = drawA
607
		anim.update = updateA
608
		anim.next = nextA
609
		anim.previous = previousA
610
		anim.moveTo = moveTo
611
		return anim
612
	else
613
		error(path.." not found!")
614
	end
615
end