aka gravity ninja golf
1extends Node2D
2
3## ff is the array of force field components (idk if theres a word for this in physics tbh)
4var ff: Array[Vector4] = []
5## pp is the array of point locations
6var pp: Array[Vector2] = []
7## pcpc is the array of point colors (well really just the alpha)
8var pcpc: Array[float] = []
9## vv is the array of point velocities
10var vv: Array[Vector2] = []
11## start is the start location (here) (probably gonna remove this)
12var mp
13## t is a time variable which we use to calculate when to resketch sketched objects
14var t = 0
15
16var FORCE_AVG: float = 15000.0
17var FORCE_DEV: float = 3000.0
18var FORCE_POWER: float = 1.5
19var MY_FRICTION: float = .9999
20var N_FIELDS = 64
21var N_POINTS = 100
22
23var startp: Vector2
24var endp: Vector2
25
26var debug = false
27var firing = false
28@onready var activeStone = null
29@onready var stones: Array[Stone] = []
30
31## BOUNDS is the screen bounds, dimensions of the game window or smth
32var BOUNDS: Rect2 = ref.BOUNDS
33## SAFEBOUNDS is for some things which we don't want to generate right next to the side of the window
34var SAFEBOUNDS: Rect2 = ref.SAFEBOUNDS
35
36var debugstarts: Array[Vector2] = []
37var debugends: Array[Vector2] = []
38
39
40func generatelevel():
41 genstartend()
42 genfields()
43
44@rpc("authority", "call_remote", "reliable")
45func server_transfer_level(_start: Vector2, _end: Vector2, _ff: PackedVector4Array):
46 pass
47
48
49func _init():
50 genstartend()
51 genfields()
52 rendervecfield()
53 pp.resize(N_POINTS)
54 pcpc.resize(N_POINTS)
55 for i in range(N_POINTS):
56 pp[i] = genpoint()
57 pcpc[i] = 2*randf() - 1
58
59 vv.resize(N_POINTS)
60 vv.fill(Vector2.ZERO)
61 mp = Vector2.ZERO
62
63func genstartend():
64 var pts = genpoints(550)
65 startp = Vector2(pts.x, pts.y)
66 endp = Vector2(pts.z, pts.w)
67
68func genfields():
69 ff.resize(N_FIELDS)
70 for i in range(N_FIELDS):
71 ff[i] = genfield()
72
73func rendervecfield():
74
75 debugstarts = []
76 debugends = []
77 # pregenerate debug field bc it's a bit costly to do each frame, and doesn't change unless ff changes
78 for x in range(BOUNDS.position.x, BOUNDS.position.x + BOUNDS.size.x, 8):
79 for y in range(BOUNDS.position.y, BOUNDS.position.y + BOUNDS.size.y, 8):
80 debugstarts.append(Vector2(x,y))
81 var v = forces(Vector2(x,y))/10
82 if v.length() > 12:
83 v = v.normalized() * 12
84 debugends.append(Vector2(x,y)+ v)
85
86func _ready():
87 $Target.position = endp
88 #$TitleScreen/Connect.clicked.connect(on_title_screen_clicked)
89 #$TitleScreen/Host.clicked.connect(on_title_screen_clicked)
90
91
92## genpoints generates 2 points that we intend to be at least target (toroidal) distance apart
93## within game bounds.
94## if it fails to generate 2 points at least target apart in maxtries, then it returns the two
95## farthest points from each other which it found.
96## returns points as a Vector4 (res.xy = p1, res.zw = p2)
97func genpoints(target: float, within: Rect2 = BOUNDS, maxtries: int = 20) -> Vector4:
98 var b1 = Vector2.ZERO
99 var b2 = Vector2.ZERO
100 var bd = 0
101 for i in range(maxtries):
102 var p1 = genpoint(within)
103 var p2 = genpoint(within)
104 var d = tDist(p1,p2).length()
105 if d > bd:
106 bd = d
107 b1 = p1
108 b2 = p2
109 if d > target:
110 break
111 return Vector4(b1.x, b1.y, b2.x, b2.y)
112
113var last_avg
114var last_dev
115var hidedebug = false
116var showdebug = false
117
118func on_title_screen_clicked():
119 $TitleScreen.hide()
120
121func _process(delta: float):
122 do_debug_inputs(delta)
123
124 t += delta
125 if t > .24:
126 for stone in stones:
127 stone.sketch()
128 $Target.sketch()
129 t = 0
130
131 mp = get_viewport().get_mouse_position()
132 if Input.is_action_pressed("click") && !firing && activeStone:
133 var correctHeading = tDist(activeStone.position, mp)
134 activeStone.set_heading(correctHeading)
135 if Input.is_action_just_released("click") && !firing && activeStone:
136 var correctHeading = tDist(activeStone.position, mp)
137 activeStone.confirm_heading(correctHeading)
138
139 if Input.is_action_just_pressed("fire") && !firing && activeStone && activeStone.hconfirmed:
140 var h = activeStone.fire()
141 firing = true
142 activeStone.velocity = h
143 activeStone.drag = 1.0
144
145 # like if firing and sweep keys are just pressed
146 # we then will decrease drag and stuff and maybe rotate it a bit
147
148
149 for stone in stones:
150 if stone.mystate == Stone.state.INIT:
151 continue
152 #var sf = .5
153 #if stone == activeStone:
154 #sf = 1.0
155 #stone.velocity = stone.velocity + forces(stone.position) * delta * sf
156 stone.velocity = stone.velocity * stone.drag
157 if firing:
158 activeStone.drag = activeStone.drag * MY_FRICTION
159 updatePositions(delta)
160 if firing:
161 if activeStone.velocity.length() < 10.0:
162 activeStone.drag = .99
163 var ns = Stone.new()
164 ns.position = genpoint(SAFEBOUNDS)
165 ns.modulate = clr.num.pick_random()
166 add_child(ns)
167 activeStone = ns
168 stones.append(ns)
169 firing = false
170 MY_FRICTION = .9999
171
172
173 # physics simulation
174 for i in range(N_POINTS):
175 var a = pcpc[i]
176 if a > 0 && a - delta/10 < 0:
177 pp[i] = genpoint()
178 vv[i] = Vector2.ZERO
179 pcpc[i] -= .001
180 if pcpc[i] < -1:
181 pcpc[i] += 2
182
183 # f = ma, m is constant, so f=a, and then we can basically integrate first order approximation
184 # to get dv = f*dt, and just add the dv to existing vv. we can use runge-kutta for player
185 # position to get increased accuracy
186 vv[i] += delta*forces(pp[i])
187
188 ## ds = v*dt, blah blah same first order stuff
189 pp[i] = tNorm(pp[i] + delta*vv[i])
190
191
192 # this is some friction air resistance stuff, it makes everything much more interesting to look at
193 vv[i] *= .99
194 #updatePositions(pp, vv, delta)
195 queue_redraw()
196
197func _draw():
198 if debug:
199 for i in range(min(debugstarts.size(), debugends.size())):
200 draw_line(debugstarts[i], debugends[i], gencolor(debugstarts[i], debugends[i]), 1, false)
201
202 for i in range(N_POINTS):
203 var c = clr.white
204 c.a = abs(pcpc[i])
205 draw_circle(pp[i], 1.0, c, 2)
206
207 if (debug && !hidedebug) || showdebug:
208 var dstring = "debug: d; hide: h"
209 var ds = font.display.get_string_size(dstring)
210 draw_rect(Rect2(Vector2(16,16), ds), clr.black)
211 draw_string(font.display, Vector2(16,32), dstring, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, clr.white)
212 dstring = "regenerate: r"
213 ds = font.display.get_string_size(dstring)
214 draw_rect(Rect2(Vector2(16,36), ds), clr.black)
215 draw_string(font.display, Vector2(16,52), dstring, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, clr.white)
216 dstring = "N_FIELDS: (up: +, down: _) = %d" % N_FIELDS
217 ds = font.display.get_string_size(dstring)
218 draw_rect(Rect2(Vector2(16,56), ds), clr.black)
219 draw_string(font.display, Vector2(16,72), dstring, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, clr.white)
220 dstring = "FORCE_AVG: (up: }, down: {) = %f" % FORCE_AVG
221 ds = font.display.get_string_size(dstring)
222 draw_rect(Rect2(Vector2(16,76), ds), clr.black)
223 draw_string(font.display, Vector2(16,92), dstring, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, clr.white)
224 dstring = "FORCE_DEV: (up: >, down: <) = %f" % FORCE_DEV
225 ds = font.display.get_string_size(dstring)
226 draw_rect(Rect2(Vector2(16,96), ds), clr.black)
227 draw_string(font.display, Vector2(16,112), dstring, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, clr.white)
228 dstring = "MY_FRICTION: (up: UP_ARROW, down: DOWN_ARROW) = %f" % MY_FRICTION
229 ds = font.display.get_string_size(dstring)
230 draw_rect(Rect2(Vector2(16,116), ds), clr.black)
231 draw_string(font.display, Vector2(16,132), dstring, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, clr.white)
232 dstring = "FORCE_POWER: (up: RIGHT_ARROW, down: LEFT_ARROW) = %f" % FORCE_POWER
233 ds = font.display.get_string_size(dstring)
234 draw_rect(Rect2(Vector2(16,136), ds), clr.black)
235 draw_string(font.display, Vector2(16,152), dstring, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, clr.white)
236
237
238func gencolor(p: Vector2, v: Vector2) -> Color:
239 var s = v - p
240 var h = s.angle()/TAU
241 var c: Color
242 if h < -0.25: c = clr.green
243 elif h < 0: c = clr.yellow
244 elif h < 0.25: c = clr.orange
245 else: c = clr.red
246 c.a = 0.25
247 return c
248
249## genpoint generates a point. by default the point is wihin BOUNDS, but we can pick any Rect2
250func genpoint(within: Rect2 = BOUNDS) -> Vector2:
251 return within.position + Vector2(randf()*within.size.x, randf()*within.size.y)
252
253## genfield generates a force field component. res.xy is the origin of the force field component
254## r is the magnitude of the field
255## t is the angle that we are acting on
256func genfield() -> Vector4:
257 var o = genpoint()
258 var r = randfn(FORCE_AVG,FORCE_DEV)
259 var theta = randf() * TAU
260 return Vector4(o.x, o.y, r, theta)
261
262## force calcultaes the force on a point [param x] given the force field component [param f] [br]
263## we basically use an inverse relation, since the top is proportional in magnitude
264## to the distance between [param x] and the origin of [param f]. [br]
265## the way to think about r and theta are that r is the magnitude of the field, and r is the direction. [br]
266## there's a bit of redundant information since the angle can go all 2*pi radians, and the magnitude
267## can be negative, but we don't care. [br]
268## the way we calculate force is that we take the distance between [param x] and o as a vector, and then rotate
269## it according theta, and then scale it according to r. so say theta = pi/2 = 90deg, and r = 1
270## since the force is perpendicular to the displacement from the origin, we'd basically be rotated
271## around the point. if theta is 120deg, and r was 1 then we'd spiral into the origin.
272## if theta was 0 degrees and r was 1, then we'd be pushed away from the origin, etc... [br]
273## i can draw some pictures of the force fields this generates if you care, but as long as they feel mostly
274## ok to play with, then we can leave this be mostly
275func force(x: Vector2, f: Vector4):
276 var o = Vector2(f.x,f.y)
277 var r = f.z
278 var theta = f.w
279 var dist = tDist(x, o)
280 return dist.normalized().rotated(theta)*r/pow(dist.length()+10,FORCE_POWER)
281
282## forces is the sum of all forces evaluated at [param x]
283func forces(x: Vector2):
284 var res = Vector2.ZERO
285 for f in ff:
286 res += force(x, f)
287 return res
288
289## tDist is the toroidal distance within BOUNDS. this is some magic formula i found more on the internet,
290## the basic idea is that since we want the left side to wrap around smoothly to the right side, and
291## the top to wrap around smoothly to the bottom, instead of taking place in a rectangle, we take place
292## in an implicit torus. points that are close to the left and right side of the screen are right next
293## to each other, even though if we calculate the normal rectangular distance, then they'd be far apart.
294## to get the proper distance, we need to find the minimum distance in the x and y directions from [param from]
295## to [param to] where [param to] is in the same x screen as [param from], in the screen 1 to left of [param from], and in the screen 1 to
296## the right of [param from], and the same vertically. calculating the distances in this manner prevents a bunch
297## of ugly if statements. more reading below [br]
298## [url]https://blog.demofox.org/2017/10/01/calculating-the-distance-between-points-in-wrap-around-toroidal-space/[/url]
299func tDist(from: Vector2, to: Vector2):
300 var dx = to.x - from.x
301 if abs(dx) > abs(dx - BOUNDS.size.x):
302 dx = dx - BOUNDS.size.x
303 elif abs(dx) > abs(dx + BOUNDS.size.x):
304 dx = dx + BOUNDS.size.x
305 var dy = to.y - from.y
306 if abs(dy) > abs(dy - BOUNDS.size.y):
307 dy = dy - BOUNDS.size.y
308 elif abs(dy) > abs(dy + BOUNDS.size.y):
309 dy = dy + BOUNDS.size.y
310 return Vector2(dx,dy)
311
312func updatePositions(delta: float):
313 var remaining: float = delta
314 while remaining > 0:
315 var pcollisiont: Array[float] = []
316 var pcollisions: Array[Vector2i] = []
317 for i in range(stones.size()):
318 for j in range(i + 1,stones.size()):
319 # https://stackoverflow.com/questions/18410937/resolving-a-circle-circle-collision
320 var p = tDist(stones[i].position, stones[j].position)
321 if p.length() < 22.0:
322 var res = tCollision(stones[i].position, stones[j].position, stones[i].velocity, stones[j].velocity)
323 stones[i].velocity = -Vector2(res.x, res.y)
324 stones[j].velocity = -Vector2(res.z, res.w)
325 continue
326 var v = stones[i].velocity - stones[j].velocity
327 var a = v.dot(v)
328 var b = 2 * p.dot(v)
329 var c = p.dot(p) - 22.0*22.0
330 var discriminant = b*b - 4 * a * c
331 if discriminant < 0:
332 continue
333 var time1 = (-b - sqrt(discriminant)) / (2 * a)
334 var time2 = (-b + sqrt(discriminant)) / (2 * a)
335 var time = time1
336 if time1 < 0:
337 time = time2
338 if time < 0:
339 continue
340 if time > remaining:
341 continue
342 else:
343 pcollisiont.append(time)
344 pcollisions.append(Vector2i(i,j))
345 var least = remaining
346 var leasti = -1
347 var leastj = -1
348 for i in range(pcollisions.size()):
349 if pcollisiont[i] < least:
350 least = pcollisiont[i]
351 leasti = pcollisions[i].x
352 leastj = pcollisions[i].y
353 remaining -= least
354 for i in range(stones.size()):
355 stones[i].position = tNorm(stones[i].position + stones[i].velocity*least)
356 if leasti > -1:
357 var res = tCollision(stones[leasti].position, stones[leastj].position, stones[leasti].velocity, stones[leastj].velocity)
358 stones[leasti].velocity = -Vector2(res.x, res.y)
359 stones[leastj].velocity = -Vector2(res.z, res.w)
360
361 #for i in range(stones.size()):
362 #for j in range(i + 1, stones.size()):
363 #var d = tDist(stones[i].position, stones[j].position)
364 #if d.length() < 21.0:
365 #var bump = (22.5 - d.length()) / 2
366 #stones[i].position = stones[i].position - bump * d.normalized()
367 #stones[j].position = stones[j].position + bump * d.normalized()
368
369
370func tCollision(apos: Vector2, bpos: Vector2, avel: Vector2, bvel: Vector2):
371 var d = tDist(apos, bpos)
372 var n = d.normalized()
373 var vrel = avel - bvel
374 var vn = vrel.dot(n)
375 if vn > 0:
376 return Vector4(avel.x, avel.y, bvel.x, bvel.y)
377 var avelf = avel - vn * n
378 var bvelf = bvel + vn * n
379 return Vector4(avelf.x, avelf.y, bvelf.x, bvelf.y)
380
381## tNorm is a bad name which takes [param p] and puts it within bounds, used whenever we move points and might cause
382## them to wrap around. might not be necessary for things which we don't render, but i think tDist requires
383## that both points are in the main universe so you probably should use me
384func tNorm(p: Vector2) -> Vector2:
385 var x = p.x
386 while x < 0:
387 x += BOUNDS.size.x
388 while x > BOUNDS.size.x:
389 x -= BOUNDS.size.x
390 var y = p.y
391 while y < 0:
392 y += BOUNDS.size.y
393 while y > BOUNDS.size.y:
394 y -= BOUNDS.size.y
395 return Vector2(x,y)
396
397func tNormConnected(u: Vector2, v: Vector2) -> bool:
398 return abs(tDist(u, v).length_squared() - (v - u).length_squared()) < 10
399
400
401func do_debug_inputs(delta: float):
402 if Input.is_action_just_pressed("click"):
403 #$TitleScreen.hide()
404 pass
405 if Input.is_action_just_pressed("debug"):
406 debug = !debug
407 hidedebug = Input.is_action_pressed("hide")
408 showdebug = Input.is_action_pressed("show")
409 if Input.is_action_just_pressed("regenerate"):
410 genfields()
411 rendervecfield()
412 if Input.is_action_pressed("fields_up"):
413 N_FIELDS += 1
414 if Input.is_action_pressed("fields_down"):
415 N_FIELDS -= 1
416 if Input.is_action_just_pressed("avg_up"):
417 last_avg = FORCE_AVG
418 if Input.is_action_just_pressed("avg_down"):
419 last_avg = FORCE_AVG
420 if Input.is_action_pressed("avg_up"):
421 FORCE_AVG *= 1.005
422 if Input.is_action_pressed("avg_down"):
423 FORCE_AVG *= .995
424 if Input.is_action_just_released("avg_up") || Input.is_action_just_released("avg_down"):
425 var del = FORCE_AVG - last_avg
426 for i in range(ff.size()):
427 var f = ff[i]
428 f.z = f.z + del
429 ff[i] = f
430 rendervecfield()
431 if Input.is_action_just_pressed("dev_up"):
432 last_dev = FORCE_DEV
433 if Input.is_action_just_pressed("dev_down"):
434 last_dev = FORCE_DEV
435 if Input.is_action_pressed("dev_up"):
436 FORCE_DEV *= 1.005
437 if Input.is_action_pressed("dev_down"):
438 FORCE_DEV *= .995
439 if Input.is_action_just_released("dev_up") || Input.is_action_just_released("dev_down"):
440 var ratio = FORCE_DEV / last_dev
441 for i in range(ff.size()):
442 var f = ff[i]
443 f.z = FORCE_AVG + (f.z-FORCE_AVG) * ratio
444 ff[i] = f
445 rendervecfield()
446 if Input.is_action_pressed("friction_up"):
447 MY_FRICTION *= .99999
448 if Input.is_action_pressed("friction_down"):
449 MY_FRICTION = lerp(MY_FRICTION, 1.0, delta)
450 if Input.is_action_pressed("power_up"):
451 FORCE_POWER *= 1.0005
452 if Input.is_action_pressed("power_down"):
453 FORCE_POWER *= .9995
454 if Input.is_action_just_released("power_up"):
455 rendervecfield()
456 if Input.is_action_just_released("power_down"):
457 rendervecfield()
458