Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/samples/expert-tankspank.rb
blob: b28f6d73b65761a59dea986f12f522943c203ab2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
# Tankspank
# kevin conner
# connerk@gmail.com
# version 3, 13 March 2008
# this code is free, do what you like with it!

$width, $height = 700, 500
$camera_tightness = 0.1

module Collisions
	def contains? x, y
		not (x < west or x > east or y < north or y > south)
	end
	
	def intersects? other
		not (other.east < west or other.west > east or
			other.south < north or other.north > south)
	end
end

class Building
	include Collisions
	
	attr_reader :west, :east, :north, :south
	
	def initialize(west, east, north, south)
		@west, @east, @north, @south = west, east, north, south
		@top, @bottom = 1.1 + rand(3) * 0.15, 1.0
		
		color = (1..3).collect { 0.2 + 0.4 * rand }
		color << 0.9
		@stroke = $app.rgb *color
		color[-1] = 0.3
		@fill = $app.rgb *color
	end
	
	def draw
		$app.stroke @stroke
		$app.fill @fill
		Opp.draw_opp_box(@west, @east, @north, @south, @top, @bottom)
	end
end

module Guidance
	def guidance_system x, y, dest_x, dest_y, angle
		vx, vy = dest_x - x, dest_y - y
		if vx.abs < 0.1 and vy.abs <= 0.1
			yield 0, 0
		else
			length = Math.sqrt(vx * vx + vy * vy)
			vx /= length
			vy /= length
			ax, ay = Math.cos(angle), Math.sin(angle)
			cos_between = vx * ax + vy * ay
			sin_between = vx * -ay + vy * ax
			yield sin_between, cos_between
		end
	end
end

module Life
	attr_reader :health
	def dead?
		@health == 0
	end
	def hurt damage
		@health = [@health - damage, 0].max
	end
end

class Tank
	include Collisions
	include Guidance
	include Life
	# ^ sounds like insurance
	
	@@collide_size = 15
	def west; @x - @@collide_size; end
	def east; @x + @@collide_size; end
	def north; @y - @@collide_size; end
	def south; @y + @@collide_size; end
	
	attr_reader :x, :y
	
	def initialize
		@x, @y = 0, -125
		@last_x, @last_y = @x, @y
		@tank_angle = 0.0
		@dest_x, @dest_y = 0, 0
		@acceleration = 0.0
		@speed = 0.0
		@moving = false
		
		@aim_angle = 0.0
		@target_x, @target_y = 0, 0
		@aimed = false
		
		@health = 100
	end
	
	def set_destination
		@dest_x, @dest_y = @target_x, @target_y
		@moving = true
	end
	
	def fire
		Opp.add_shell Shell.new(@x + 30 * Math.cos(@aim_angle),
			@y + 30 * Math.sin(@aim_angle), @aim_angle)
	end
	
	def update button, mouse_x, mouse_y
		@target_x, @target_y = mouse_x, mouse_y
		
		if @moving
			guidance_system @x, @y, @dest_x, @dest_y, @tank_angle do |direction, on_target|
				turn direction
				@acceleration = on_target * 0.25
			end
			
			distance = Math.sqrt((@dest_x - @x) ** 2 + (@dest_y - @y) ** 2)
			@moving = false if distance < 50
		else
			@acceleration = 0.0
		end
		
		guidance_system @x, @y, @target_x, @target_y, @aim_angle do |direction, on_target|
			aim direction
			@aimed = on_target > 0.98
		end
		
		integrity = @health / 100.0 # the more hurt you are, the slower you go
		@speed = [[@speed + @acceleration, 5.0 * integrity].min, -3.0 * integrity].max
		@speed *= 0.9 if !@moving
		
		@last_x, @last_y = @x, @y
		@x += @speed * Math.cos(@tank_angle)
		@y += @speed * Math.sin(@tank_angle)
	end
	
	def collide_and_stop
		@x, @y = @last_x, @last_y
		hurt @speed.abs * 3 + 5
		@speed = 0
		@moving = false
	end
	
	def turn direction
		@tank_angle += [[-0.03, direction].max, 0.03].min
	end
	
	def aim direction
		@aim_angle += [[-0.1, direction].max, 0.1].min
	end
	
	def draw
		$app.stroke $app.blue
		$app.fill $app.blue(0.4)
		Opp.draw_opp_rect @x - 20, @x + 20, @y - 15, @y + 15, 1.05, @tank_angle
		#Opp.draw_opp_box @x - 20, @x + 20, @y - 20, @y + 20, 1.03, 1.0
		Opp.draw_opp_rect @x - 10, @x + 10, @y - 7, @y + 7, 1.05, @aim_angle
		x, unused1, y, unused2 = Opp.project(@x, 0, @y, 0, 1.05)
		$app.line x, y, x + 25 * Math.cos(@aim_angle), y + 25 * Math.sin(@aim_angle)
		
		$app.stroke $app.red
		$app.fill $app.red(@aimed ? 0.4 : 0.1)
		Opp.draw_opp_oval @target_x - 10, @target_x + 10, @target_y - 10, @target_y + 10, 1.00
		
		if @moving
			$app.stroke $app.green
			$app.fill $app.green(0.2)
			Opp.draw_opp_oval @dest_x - 20, @dest_x + 20, @dest_y - 20, @dest_y + 20, 1.00
		end
	end
end

class Shell
	attr_reader :x, :y
	
	def initialize x, y, angle
		@x, @y, @angle = x, y, angle
		@speed = 10.0
	end
	
	def update
		@x += @speed * Math.cos(@angle)
		@y += @speed * Math.sin(@angle)
	end
	
	def draw
		$app.stroke $app.red
		$app.fill $app.red(0.1)
		Opp.draw_opp_box @x - 2, @x + 2, @y - 2, @y + 2, 1.05, 1.04
	end
end

class Opp
	def self.new_game
		@offset_x, @offset_y = 0, 0
		@buildings = [
			[-1000, -750, -750, -250],
			[-500, 250, -750, -250],
			[500, 1000, -750, -500],
			[750, 1250, -250, 0],
			[750, 1250, 250, 750],
			[250, 500, 0, 750],
			[-250, 0, 0, 500],
			[-500, 0, 750, 1000],
			[-1000, -500, 0, 500],
			[400, 600, -350, -150]
		].collect { |p| Building.new *p }
		@shells = []
		@boundary = [-1250, 1500, -1250, 1250]
		@tank = Tank.new
		@center_x, @center_y = $app.width / 2, $app.height / 2
	end
	
	def self.tank
		@tank
	end
	
	def self.read_input
		@input = $app.mouse
	end
	
	def self.update_scene
		button, x, y = @input
		x += @offset_x - @center_x
		y += @offset_y - @center_y
		
		@tank.update(button, x, y) if !@tank.dead?
		@buildings.each do |b|
			@tank.collide_and_stop if b.intersects? @tank
		end
		
		@shells.each { |s| s.update }
		@buildings.each do |b|
			@shells.reject! do |s|
				b.contains?(s.x, s.y)
			end
		end
		#collide shells with tanks -- don't need this until there are enemy tanks
		#@shells.reject! do |s|
		#	@tank.contains?(s.x, s.y)
		#end
		
		$app.clear do
			@offset_x += $camera_tightness * (@tank.x - @offset_x)
			@offset_y += $camera_tightness * (@tank.y - @offset_y)
			
			$app.background $app.black
			@center_x, @center_y = $app.width / 2, $app.height / 2
			
			$app.stroke $app.red(0.9)
			$app.nofill
			draw_opp_box *(@boundary + [1.1, 1.0, false])
			
			@tank.draw
			@shells.each { |s| s.draw }
			@buildings.each { |b| b.draw }
		end
	end
	
	def self.add_shell shell
		@shells << shell
		@shells.shift if @shells.size > 10
	end
	
	def self.project left, right, top, bottom, depth
		[left, right].collect { |x| @center_x + depth * (x - @offset_x) } +
			[top, bottom].collect { |y| @center_y + depth * (y - @offset_y) }
	end
	
	# here "front" and "back" push the rect into and out of the window.
	# 1.0 means your x and y units are pixels on the surface.
	# greater than that brings the box closer.  less pushes it back.  0.0 => infinity.
	# the front will be filled but the rest is wireframe only.
	def self.draw_opp_box left, right, top, bottom, front, back, occlude = true
		near_left, near_right, near_top, near_bottom = project(left, right, top, bottom, front)
		far_left, far_right, far_top, far_bottom = project(left, right, top, bottom, back)
		
		# determine which sides of the box are visible
		if occlude
			draw_left = @center_x < near_left
			draw_right = near_right < @center_x
			draw_top = @center_y < near_top
			draw_bottom = near_bottom < @center_y
		else
			draw_left, draw_right, draw_top, draw_bottom = [true] * 4
		end
		
		# draw lines for the back edges
		$app.line far_left, far_top, far_right, far_top if draw_top
		$app.line far_left, far_bottom, far_right, far_bottom if draw_bottom
		$app.line far_left, far_top, far_left, far_bottom if draw_left
		$app.line far_right, far_top, far_right, far_bottom if draw_right
		
		# draw lines to connect the front and back
		$app.line near_left, near_top, far_left, far_top if draw_left or draw_top
		$app.line near_right, near_top, far_right, far_top if draw_right or draw_top
		$app.line near_left, near_bottom, far_left, far_bottom if draw_left or draw_bottom
		$app.line near_right, near_bottom, far_right, far_bottom if draw_right or draw_bottom
		
		# draw the front, filled
		$app.rect near_left, near_top, near_right - near_left, near_bottom - near_top
	end
	
	def self.draw_opp_rect left, right, top, bottom, depth, angle, with_x = false
		pl, pr, pt, pb = project(left, right, top, bottom, depth)
		cos = Math.cos(angle)
		sin = Math.sin(angle)
		cx, cy = (pr + pl) / 2.0, (pb + pt) / 2.0
		points = [[pl, pt], [pr, pt], [pr, pb], [pl, pb]].collect do |x, y|
			[cx + (x - cx) * cos - (y - cy) * sin,
				cy + (x - cx) * sin + (y - cy) * cos]
		end
		
		$app.line *(points[0] + points[1])
		$app.line *(points[1] + points[2])
		$app.line *(points[2] + points[3])
		$app.line *(points[3] + points[0])
	end
	
	def self.draw_opp_oval left, right, top, bottom, depth
		pl, pr, pt, pb = project(left, right, top, bottom, depth)
		$app.oval(pl, pt, pr - pl, pb - pt)
	end
	
	def self.draw_opp_plane x1, y1, x2, y2, front, back, stroke_color
		near_x1, near_x2, near_y1, near_y2 = project(x1, x2, y1, y2, front)
		far_x1, far_x2, far_y1, far_y2 = project(x1, x2, y1, y2, back)
		
		$app.stroke stroke_color
		
		$app.line far_x1, far_y1, far_x2, far_y2
		$app.line far_x1, far_y1, near_x1, near_y1
		$app.line far_x2, far_y2, near_x2, near_y2
		$app.line near_x1, near_y1, near_x2, near_y2
	end
end

Shoes.app :width => $width, :height => $height do
	$app = self
	
	Opp.new_game
	@playing = true
	
	keypress do |key|
		if @playing
			if key == "1" or key == "z"
				Opp.tank.set_destination
			elsif key == "2" or key == "x" or key == " "
				Opp.tank.fire
			end
		else
			if key == "n"
				Opp.new_game
				@playing = true
			end
		end
	end
	
	click do |button, x, y|
		if @playing
			if button == 1
				Opp.tank.set_destination
			else
				Opp.tank.fire
			end
		end
	end
	
	game_over_count = -1
	animate(60) do
		Opp.read_input if @playing
		Opp.update_scene
		
		@playing = false if Opp.tank.dead?
		if !@playing
			stack do
				banner "Game Over", :stroke => white, :margin => 10
				caption "learn to drive!", :stroke => white, :margin => 20
			end
		end
	end
end