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