quake-rb 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,20 +5,41 @@ module Quake
5
5
  # Full Quake player physics: gravity, friction, acceleration, jumping,
6
6
  # ground detection, stair stepping, and swimming.
7
7
  class Player
8
- attr_accessor :position, :velocity, :on_ground, :water_level, :noclip
8
+ attr_accessor :position, :velocity, :on_ground, :water_level, :water_type, :noclip, :gravity
9
+ attr_accessor :water_jump, :water_jump_movedir, :water_jump_time
10
+ attr_reader :jump_held
9
11
  attr_reader :yaw, :pitch
10
12
 
11
13
  # Quake constants
12
14
  GRAVITY = 800.0 # units/sec^2
15
+ STOP_EPSILON = 0.1
13
16
  FRICTION = 4.0
17
+ EDGE_FRICTION = 2.0
14
18
  STOP_SPEED = 100.0
15
19
  MAX_SPEED = 320.0
20
+ MAX_VELOCITY = 2000.0
21
+ FORWARD_SPEED = 200.0
22
+ BACK_SPEED = 200.0
23
+ SIDE_SPEED = 350.0
24
+ UP_SPEED = 200.0
16
25
  ACCELERATE = 10.0
17
- AIR_ACCELERATE = 0.7
26
+ AIR_ACCELERATE = 10.0
18
27
  JUMP_SPEED = 270.0
19
28
  STEP_SIZE = 18.0
20
- WATER_FRICTION = 1.0
29
+ WATER_FRICTION = 4.0
21
30
  WATER_ACCELERATE = 10.0
31
+ UNSTICK_DISTANCE = 2.0
32
+ MIN_UNSTICK_PROGRESS = 4.0
33
+ UNSTICK_DIRECTIONS = [
34
+ Math::Vec3.new(UNSTICK_DISTANCE, 0.0, 0.0),
35
+ Math::Vec3.new(0.0, UNSTICK_DISTANCE, 0.0),
36
+ Math::Vec3.new(-UNSTICK_DISTANCE, 0.0, 0.0),
37
+ Math::Vec3.new(0.0, -UNSTICK_DISTANCE, 0.0),
38
+ Math::Vec3.new(UNSTICK_DISTANCE, UNSTICK_DISTANCE, 0.0),
39
+ Math::Vec3.new(-UNSTICK_DISTANCE, UNSTICK_DISTANCE, 0.0),
40
+ Math::Vec3.new(UNSTICK_DISTANCE, -UNSTICK_DISTANCE, 0.0),
41
+ Math::Vec3.new(-UNSTICK_DISTANCE, -UNSTICK_DISTANCE, 0.0)
42
+ ].freeze
22
43
 
23
44
  # Camera
24
45
  SENSITIVITY = 0.15
@@ -28,6 +49,16 @@ module Quake
28
49
  # Ground: surface normal Z must be > 0.7 (roughly < 45 degrees from horizontal)
29
50
  MIN_GROUND_NORMAL_Z = 0.7
30
51
 
52
+ # SV_FlyMove (sv_phys.c)
53
+ MAX_CLIP_PLANES = 5
54
+ CLIP_FLOOR = 1 << 0
55
+ CLIP_WALL = 1 << 1
56
+ CLIP_STOP = 1 << 2
57
+
58
+ # CheckWaterJump (client.qc)
59
+ WATER_JUMP_SPEED = 225.0
60
+ WATER_JUMP_TIME = 2.0 # teleport_time safety net: time + 2
61
+
31
62
  def initialize(position:, yaw: 0.0)
32
63
  @position = position
33
64
  @velocity = Math::Vec3::ORIGIN
@@ -35,9 +66,14 @@ module Quake
35
66
  @pitch = 0.0
36
67
  @on_ground = false
37
68
  @water_level = 0 # 0=dry, 1=feet, 2=waist, 3=head
69
+ @water_type = CONTENTS_EMPTY
38
70
  @jump_held = false
39
71
  @noclip = false
72
+ @gravity = GRAVITY
40
73
  @ignore_mouse = 2
74
+ @water_jump = false
75
+ @water_jump_movedir = Math::Vec3::ORIGIN
76
+ @water_jump_time = 0.0
41
77
  end
42
78
 
43
79
  # Camera eye position (origin + view height)
@@ -64,23 +100,33 @@ module Quake
64
100
  end
65
101
 
66
102
  @brush_entities = brush_entities
103
+ check_velocity
67
104
  categorize_position(level)
68
105
 
69
106
  # Calculate wish direction from input
70
107
  wish_dir, wish_speed = compute_wish_velocity(keys)
71
108
 
72
- if @water_level >= 2
109
+ # QuakeC PlayerPreThink: probe for a jump-out-of-water while swimming
110
+ check_water_jump(level) if @water_level == 2 && !@water_jump
111
+
112
+ if @water_jump
113
+ # SV_ClientThink short-circuits friction/accel while FL_WATERJUMP
114
+ # is set (sv_user.c:357-360); SV_Physics_Client skips gravity.
115
+ @water_jump_time -= dt
116
+ water_jump_move
117
+ elsif @water_level >= 2
73
118
  water_move(dt, wish_dir, wish_speed, keys, level)
74
119
  else
75
120
  # Apply gravity if not on ground and not in water
76
121
  unless @on_ground
77
122
  @velocity = Math::Vec3.new(@velocity.x, @velocity.y,
78
- @velocity.z - GRAVITY * dt)
123
+ @velocity.z - @gravity * dt)
79
124
  end
80
125
 
81
- # Handle jumping (SET vz, don't add - matches Quake's PM_AirMove)
126
+ # Handle jumping (QuakeC PlayerJump: velocity_z = velocity_z + 270)
82
127
  if @on_ground && keys[SDL::SCANCODE_SPACE] && !@jump_held
83
- @velocity = Math::Vec3.new(@velocity.x, @velocity.y, JUMP_SPEED)
128
+ @velocity = Math::Vec3.new(@velocity.x, @velocity.y,
129
+ @velocity.z + JUMP_SPEED)
84
130
  @on_ground = false
85
131
  @jump_held = true
86
132
  end
@@ -122,6 +168,16 @@ module Quake
122
168
  Math::Vec3.new(::Math.cos(ry), ::Math.sin(ry), 0.0)
123
169
  end
124
170
 
171
+ def up
172
+ ry = deg2rad(@yaw)
173
+ rp = deg2rad(@pitch)
174
+ Math::Vec3.new(
175
+ ::Math.cos(ry) * ::Math.sin(rp),
176
+ ::Math.sin(ry) * ::Math.sin(rp),
177
+ ::Math.cos(rp)
178
+ )
179
+ end
180
+
125
181
  private
126
182
 
127
183
  def deg2rad(deg)
@@ -129,30 +185,72 @@ module Quake
129
185
  end
130
186
 
131
187
  def compute_wish_velocity(keys)
188
+ # SV_AirMove (sv_user.c): AngleVectors uses angles[PITCH] =
189
+ # -v_angle[PITCH] / 3, then wishvel[2] is forced to 0 for
190
+ # MOVETYPE_WALK. Net effect: the forward contribution's horizontal
191
+ # magnitude scales by cos(pitch / 3); right stays flat (roll is 0).
192
+ fwd = forward_flat * ::Math.cos(deg2rad(@pitch / 3.0))
193
+
132
194
  wish = Math::Vec3::ORIGIN
133
- wish = wish + forward_flat if keys[SDL::SCANCODE_W]
134
- wish = wish - forward_flat if keys[SDL::SCANCODE_S]
135
- wish = wish + right_flat if keys[SDL::SCANCODE_D]
136
- wish = wish - right_flat if keys[SDL::SCANCODE_A]
195
+ wish = wish + fwd * FORWARD_SPEED if keys[SDL::SCANCODE_W]
196
+ wish = wish - fwd * BACK_SPEED if keys[SDL::SCANCODE_S]
197
+ wish = wish + right_flat * SIDE_SPEED if keys[SDL::SCANCODE_D]
198
+ wish = wish - right_flat * SIDE_SPEED if keys[SDL::SCANCODE_A]
137
199
 
138
200
  len = wish.length
139
201
  return [Math::Vec3::ORIGIN, 0.0] if len < 0.001
140
202
 
141
- [wish.normalize, MAX_SPEED]
203
+ wish_speed = [len, MAX_SPEED].min
204
+ [wish.normalize, wish_speed]
205
+ end
206
+
207
+ def check_velocity
208
+ @position = Math::Vec3.new(
209
+ quake_checked_position_component(@position.x),
210
+ quake_checked_position_component(@position.y),
211
+ quake_checked_position_component(@position.z)
212
+ )
213
+ @velocity = Math::Vec3.new(
214
+ quake_checked_velocity_component(@velocity.x),
215
+ quake_checked_velocity_component(@velocity.y),
216
+ quake_checked_velocity_component(@velocity.z)
217
+ )
218
+ end
219
+
220
+ def quake_checked_position_component(value)
221
+ value.nan? ? 0.0 : value
222
+ end
223
+
224
+ def quake_checked_velocity_component(value)
225
+ return 0.0 if value.nan?
226
+
227
+ value.clamp(-MAX_VELOCITY, MAX_VELOCITY)
142
228
  end
143
229
 
144
230
  def apply_friction(dt, level)
145
231
  speed = ::Math.sqrt(@velocity.x**2 + @velocity.y**2)
146
- return if speed < 1.0
232
+ return if speed.zero?
233
+
234
+ friction = FRICTION
235
+ if level
236
+ probe_x = @position.x + @velocity.x / speed * 16.0
237
+ probe_y = @position.y + @velocity.y / speed * 16.0
238
+ start = Math::Vec3.new(probe_x, probe_y, @position.z - 24.0)
239
+ stop = Math::Vec3.new(probe_x, probe_y, start.z - 34.0)
240
+ # SV_UserFriction uses SV_TraceLine (point trace, MOVE_NOMONSTERS):
241
+ # hull 0 against world + SOLID_BSP brush entities, not the box hull.
242
+ trace = trace_line(level, start, stop)
243
+ friction *= EDGE_FRICTION if trace.fraction == 1.0
244
+ end
147
245
 
148
246
  control = [speed, STOP_SPEED].max
149
- drop = control * FRICTION * dt
247
+ drop = control * friction * dt
150
248
 
151
249
  new_speed = [speed - drop, 0.0].max / speed
152
250
  @velocity = Math::Vec3.new(
153
251
  @velocity.x * new_speed,
154
252
  @velocity.y * new_speed,
155
- @velocity.z
253
+ @velocity.z * new_speed
156
254
  )
157
255
  end
158
256
 
@@ -176,75 +274,74 @@ module Quake
176
274
  end
177
275
 
178
276
  def water_move(dt, wish_dir, wish_speed, keys, level)
179
- # Water friction
180
- speed = @velocity.length
181
- if speed > 0
182
- new_speed = [speed - dt * speed * WATER_FRICTION * @water_level, 0.0].max
183
- @velocity = @velocity * (new_speed / speed)
184
- end
185
-
186
- # Build wish velocity from horizontal input + vertical keys
187
- has_horizontal = wish_speed > 0.001
188
- has_up = keys[SDL::SCANCODE_SPACE]
189
- has_down = keys[SDL::SCANCODE_C]
190
-
191
- # Use forward direction including pitch when swimming
277
+ # WinQuake SV_WaterMove builds a full wish velocity first, then
278
+ # applies water friction and accelerates toward that wish velocity.
192
279
  swim_wish = Math::Vec3::ORIGIN
193
280
  if keys[SDL::SCANCODE_W]
194
- swim_wish = swim_wish + forward
281
+ swim_wish = swim_wish + forward * FORWARD_SPEED
195
282
  end
196
283
  if keys[SDL::SCANCODE_S]
197
- swim_wish = swim_wish - forward
284
+ swim_wish = swim_wish - forward * BACK_SPEED
198
285
  end
199
286
  if keys[SDL::SCANCODE_D]
200
- swim_wish = swim_wish + right
287
+ swim_wish = swim_wish + right * SIDE_SPEED
201
288
  end
202
289
  if keys[SDL::SCANCODE_A]
203
- swim_wish = swim_wish - right
290
+ swim_wish = swim_wish - right * SIDE_SPEED
291
+ end
292
+
293
+ if swim_wish == Math::Vec3::ORIGIN && !keys[SDL::SCANCODE_SPACE] && !keys[SDL::SCANCODE_C]
294
+ swim_wish = Math::Vec3.new(0.0, 0.0, -60.0)
295
+ else
296
+ swim_wish = Math::Vec3.new(swim_wish.x, swim_wish.y, swim_wish.z + UP_SPEED) if keys[SDL::SCANCODE_SPACE]
297
+ swim_wish = Math::Vec3.new(swim_wish.x, swim_wish.y, swim_wish.z - UP_SPEED) if keys[SDL::SCANCODE_C]
204
298
  end
205
299
 
206
- # Vertical movement
207
- if has_up
208
- swim_wish = Math::Vec3.new(swim_wish.x, swim_wish.y, swim_wish.z + 1.0)
209
- elsif has_down
210
- swim_wish = Math::Vec3.new(swim_wish.x, swim_wish.y, swim_wish.z - 1.0)
300
+ wish_len = swim_wish.length
301
+ if wish_len > MAX_SPEED
302
+ swim_wish = swim_wish * (MAX_SPEED / wish_len)
303
+ wish_len = MAX_SPEED
211
304
  end
305
+ wish_speed2 = wish_len * 0.7
212
306
 
213
- swim_len = swim_wish.length
214
- if swim_len < 0.001
215
- # No input at all: drift down slowly
216
- @velocity = Math::Vec3.new(@velocity.x, @velocity.y, @velocity.z - 60.0 * dt)
217
- return
307
+ speed = @velocity.length
308
+ new_speed = 0.0
309
+ if speed.positive?
310
+ new_speed = [speed - dt * speed * WATER_FRICTION, 0.0].max
311
+ @velocity = @velocity * (new_speed / speed)
218
312
  end
219
313
 
314
+ return if wish_speed2.zero?
315
+
316
+ add_speed = wish_speed2 - new_speed
317
+ return if add_speed <= 0
318
+
220
319
  wish_dir2 = swim_wish.normalize
221
- wish_spd = MAX_SPEED * 0.7 # water reduces max speed to 70%
222
- accelerate(wish_dir2, wish_spd, WATER_ACCELERATE, dt)
320
+ accel_speed = [WATER_ACCELERATE * wish_speed2 * dt, add_speed].min
321
+ @velocity = @velocity + wish_dir2 * accel_speed
223
322
  end
224
323
 
225
324
  def categorize_position(level)
226
- # Check ground: trace 1 unit down against world + brush entities
227
- if @velocity.z > 180
228
- @on_ground = false
229
- else
230
- trace_start = @position
231
- trace_end = Math::Vec3.new(@position.x, @position.y, @position.z - 1.0)
232
- result = HullTrace.trace_world_and_entities(
233
- level, trace_start, trace_end, @brush_entities
234
- )
235
-
236
- if result.fraction < 1.0 && result.plane_normal &&
237
- result.plane_normal.z >= MIN_GROUND_NORMAL_Z
238
- @on_ground = true
239
- @position = result.end_pos if !result.start_solid && !result.all_solid
240
- # Zero out negative z velocity when grounded (matches Quake's
241
- # PM_CategorizePosition: pmove.velocity[2] = 0)
242
- if @velocity.z < 0
243
- @velocity = Math::Vec3.new(@velocity.x, @velocity.y, 0.0)
244
- end
245
- else
246
- @on_ground = false
325
+ # Check ground: trace 1 unit down against world + brush entities.
326
+ # NQ has no "vz > 180 means airborne" rule (that is QuakeWorld
327
+ # prediction); ground state comes from the down trace alone.
328
+ trace_start = @position
329
+ trace_end = Math::Vec3.new(@position.x, @position.y, @position.z - 1.0)
330
+ result = HullTrace.trace_world_and_entities(
331
+ level, trace_start, trace_end, @brush_entities
332
+ )
333
+
334
+ if result.fraction < 1.0 && result.plane_normal &&
335
+ result.plane_normal.z > MIN_GROUND_NORMAL_Z
336
+ @on_ground = true
337
+ @position = result.end_pos if !result.start_solid && !result.all_solid
338
+ # Zero out negative z velocity when grounded (matches Quake's
339
+ # PM_CategorizePosition: pmove.velocity[2] = 0)
340
+ if @velocity.z < 0
341
+ @velocity = Math::Vec3.new(@velocity.x, @velocity.y, 0.0)
247
342
  end
343
+ else
344
+ @on_ground = false
248
345
  end
249
346
 
250
347
  # Check water level using BSP leaf contents (not clipnodes).
@@ -278,79 +375,279 @@ module Quake
278
375
  leaf ? leaf.contents : CONTENTS_EMPTY
279
376
  end
280
377
 
378
+ # SV_WalkMove (sv_phys.c:887-990)
281
379
  def walk_move(dt, level)
282
- # Desired new position
283
- desired = @position + @velocity * dt
380
+ # Do a regular slide move unless it looks like you ran into a step.
381
+ # C clears FL_ONGROUND here and relies on the always-applied gravity
382
+ # to press the player into the floor so SV_FlyMove re-sets it every
383
+ # frame. This port skips gravity while grounded, so ground state is
384
+ # re-derived by categorize_position instead of cleared here.
385
+ old_on_ground = @on_ground
386
+
387
+ old_position = @position
388
+ old_velocity = @velocity
389
+
390
+ clip = fly_move(dt, level)
391
+ return if (clip & CLIP_WALL).zero?
392
+
393
+ # Don't stair up while jumping
394
+ return if !old_on_ground && @water_level.zero?
395
+ # Don't stair up while water jumping
396
+ return if @water_jump
397
+
398
+ no_step_position = @position
399
+ no_step_velocity = @velocity
400
+
401
+ # Try moving up and forward to go up a step
402
+ @position = old_position
403
+ up_trace = trace_move(level, @position,
404
+ Math::Vec3.new(@position.x, @position.y, @position.z + STEP_SIZE))
405
+ @position = up_trace.end_pos
406
+
407
+ # Move forward (full fly move, sv_phys.c:944)
408
+ @velocity = Math::Vec3.new(old_velocity.x, old_velocity.y, 0.0)
409
+ clip = fly_move(dt, level)
410
+ step_trace = @step_trace
411
+
412
+ # Check for stuckness, possibly due to the limited precision of
413
+ # floats in the clipping hulls
414
+ if clip != 0 &&
415
+ (old_position.y - @position.y).abs < DIST_EPSILON &&
416
+ (old_position.x - @position.x).abs < DIST_EPSILON
417
+ # Stepping up didn't make any progress
418
+ clip = try_unstick(level, old_velocity)
419
+ end
284
420
 
285
- # Trace against world + brush entities
286
- result = trace_move(level, @position, desired)
421
+ # Extra friction based on view angle
422
+ if (clip & CLIP_WALL) != 0 && step_trace&.plane_normal
423
+ apply_wall_friction(step_trace.plane_normal)
424
+ end
287
425
 
288
- return if result.all_solid # stuck
426
+ # Move down
427
+ step_down = Math::Vec3.new(@position.x, @position.y,
428
+ @position.z - STEP_SIZE + old_velocity.z * dt)
429
+ down_trace = trace_move(level, @position, step_down)
430
+ @position = down_trace.end_pos
289
431
 
290
- if result.fraction == 1.0
291
- @position = desired
432
+ if down_trace.fraction < 1.0 && down_trace.plane_normal &&
433
+ down_trace.plane_normal.z > MIN_GROUND_NORMAL_Z
434
+ @on_ground = true
292
435
  return
293
436
  end
294
437
 
295
- # Hit something. Try stair stepping.
296
- contact = result.end_pos
438
+ # If the push down didn't end up on good ground, use the move
439
+ # without the step up
440
+ @position = no_step_position
441
+ @velocity = no_step_velocity
442
+ end
297
443
 
298
- # Step up
299
- step_up = Math::Vec3.new(contact.x, contact.y, contact.z + STEP_SIZE)
300
- up_trace = trace_move(level, contact, step_up)
301
- raised = up_trace.end_pos
444
+ # SV_FlyMove (sv_phys.c:266-394): the basic solid body movement clip
445
+ # that slides along multiple planes. Returns CLIP_* blocked flags,
446
+ # sets @on_ground on floor hits, and stores the last wall hit in
447
+ # @step_trace for SV_WallFriction.
448
+ def fly_move(dt, level)
449
+ blocked = 0
450
+ original_velocity = @velocity
451
+ primal_velocity = @velocity
452
+ planes = []
453
+ time_left = dt
454
+ @step_trace = nil
455
+
456
+ 4.times do
457
+ break if @velocity == Math::Vec3::ORIGIN
458
+
459
+ desired = @position + @velocity * time_left
460
+ trace = trace_move(level, @position, desired)
461
+
462
+ if trace.all_solid
463
+ # Entity is trapped in another solid
464
+ @velocity = Math::Vec3::ORIGIN
465
+ return CLIP_FLOOR | CLIP_WALL
466
+ end
302
467
 
303
- # Move forward from raised position
304
- step_forward = Math::Vec3.new(desired.x, desired.y, raised.z)
305
- forward_trace = trace_move(level, raised, step_forward)
306
- forward_pos = forward_trace.end_pos
468
+ if trace.fraction.positive?
469
+ # Actually covered some distance
470
+ @position = trace.end_pos
471
+ original_velocity = @velocity
472
+ planes.clear
473
+ end
307
474
 
308
- # Step back down
309
- step_down = Math::Vec3.new(forward_pos.x, forward_pos.y, forward_pos.z - STEP_SIZE)
310
- down_trace = trace_move(level, forward_pos, step_down)
475
+ # Moved the entire distance
476
+ break if trace.fraction == 1.0
311
477
 
312
- if down_trace.fraction < 1.0 && down_trace.plane_normal &&
313
- down_trace.plane_normal.z >= MIN_GROUND_NORMAL_Z
314
- @position = down_trace.end_pos
315
- return
478
+ normal = trace.plane_normal
479
+ break unless normal
480
+
481
+ if normal.z > MIN_GROUND_NORMAL_Z
482
+ blocked |= CLIP_FLOOR
483
+ @on_ground = true
484
+ end
485
+ if normal.z.zero?
486
+ blocked |= CLIP_WALL
487
+ # Save for player extrafriction
488
+ @step_trace = trace
489
+ end
490
+
491
+ time_left -= time_left * trace.fraction
492
+
493
+ # Clipped to another plane
494
+ if planes.size >= MAX_CLIP_PLANES
495
+ # This shouldn't really happen...
496
+ @velocity = Math::Vec3::ORIGIN
497
+ return CLIP_FLOOR | CLIP_WALL
498
+ end
499
+ planes << normal
500
+
501
+ # Modify original_velocity so it parallels all of the clip planes
502
+ new_velocity = nil
503
+ index = planes.size
504
+ planes.each_with_index do |plane, i|
505
+ new_velocity = clip_velocity(original_velocity, plane)
506
+ ok = planes.each_with_index.all? do |other, j|
507
+ j == i || new_velocity.dot(other) >= 0
508
+ end
509
+ if ok
510
+ index = i
511
+ break
512
+ end
513
+ end
514
+
515
+ if index < planes.size
516
+ # Go along this plane
517
+ @velocity = new_velocity
518
+ else
519
+ # Go along the crease
520
+ if planes.size != 2
521
+ @velocity = Math::Vec3::ORIGIN
522
+ return CLIP_FLOOR | CLIP_WALL | CLIP_STOP
523
+ end
524
+ dir = planes[0].cross(planes[1])
525
+ @velocity = dir * dir.dot(@velocity)
526
+ end
527
+
528
+ # If velocity is against the original velocity, stop dead to
529
+ # avoid tiny oscillations in sloping corners
530
+ if @velocity.dot(primal_velocity) <= 0
531
+ @velocity = Math::Vec3::ORIGIN
532
+ return blocked
533
+ end
316
534
  end
317
535
 
318
- # Step didn't work - slide along the wall
319
- @position = contact
320
- slide_along_wall(result, dt, level)
536
+ blocked
537
+ end
538
+
539
+ # SV_TryUnstick (sv_phys.c:795-874): push one pixel in each direction
540
+ # and retry the move; hack around limited float precision in the hulls.
541
+ def try_unstick(level, old_velocity)
542
+ old_position = @position
543
+
544
+ UNSTICK_DIRECTIONS.each do |dir|
545
+ push_trace = trace_move(level, @position, @position + dir)
546
+ @position = push_trace.end_pos
547
+
548
+ # Retry the original move
549
+ @velocity = Math::Vec3.new(old_velocity.x, old_velocity.y, 0.0)
550
+ clip = fly_move(0.1, level)
551
+
552
+ if (old_position.y - @position.y).abs > MIN_UNSTICK_PROGRESS ||
553
+ (old_position.x - @position.x).abs > MIN_UNSTICK_PROGRESS
554
+ return clip
555
+ end
556
+
557
+ # Go back to the original pos and try again
558
+ @position = old_position
559
+ end
560
+
561
+ # Still can't move
562
+ @velocity = Math::Vec3::ORIGIN
563
+ CLIP_FLOOR | CLIP_WALL | CLIP_STOP
321
564
  end
322
565
 
323
566
  def trace_move(level, from, to)
324
567
  HullTrace.trace_world_and_entities(level, from, to, @brush_entities)
325
568
  end
326
569
 
327
- def slide_along_wall(trace_result, dt, level)
328
- normal = trace_result.plane_normal
329
- return unless normal
570
+ # SV_TraceLine with MOVE_NOMONSTERS: point (hull 0) trace against the
571
+ # world and SOLID_BSP brush entities only.
572
+ def trace_line(level, from, to)
573
+ HullTrace.trace_world_and_entities(level, from, to, @brush_entities, hull_num: 0)
574
+ end
330
575
 
331
- backoff = @velocity.dot(normal)
332
- @velocity = Math::Vec3.new(
333
- @velocity.x - normal.x * backoff,
334
- @velocity.y - normal.y * backoff,
335
- @velocity.z - normal.z * backoff
576
+ # QuakeC CheckWaterJump (client.qc): when swimming against a wall
577
+ # that is open at eye level, boost out of the water.
578
+ def check_water_jump(level)
579
+ return unless level
580
+
581
+ fwd = forward_flat
582
+
583
+ # Check for a jump-out-of-water: solid at waist?
584
+ start = Math::Vec3.new(@position.x, @position.y, @position.z + 8.0)
585
+ waist_trace = trace_line(level, start, start + fwd * 24.0)
586
+ return unless waist_trace.fraction < 1.0 && waist_trace.plane_normal
587
+
588
+ # self.movedir = trace_plane_normal * -50
589
+ @water_jump_movedir = waist_trace.plane_normal * -50.0
590
+
591
+ # Open at eye level? (start_z accumulates: origin + 8 + maxs_z - 8)
592
+ eye_start = Math::Vec3.new(@position.x, @position.y, @position.z + 32.0)
593
+ eye_trace = trace_line(level, eye_start, eye_start + fwd * 24.0)
594
+ return unless eye_trace.fraction == 1.0
595
+
596
+ @water_jump = true
597
+ @velocity = Math::Vec3.new(@velocity.x, @velocity.y, WATER_JUMP_SPEED)
598
+ @water_jump_time = WATER_JUMP_TIME
599
+ end
600
+
601
+ # SV_WaterJump (sv_user.c:264-273): while FL_WATERJUMP is set, force
602
+ # horizontal velocity from movedir every frame; clear the flag once
603
+ # out of water or past the safety-net time.
604
+ def water_jump_move
605
+ if @water_jump_time <= 0.0 || @water_level.zero?
606
+ @water_jump = false
607
+ @water_jump_time = 0.0
608
+ end
609
+ @velocity = Math::Vec3.new(@water_jump_movedir.x, @water_jump_movedir.y,
610
+ @velocity.z)
611
+ end
612
+
613
+ def clip_velocity(velocity, normal)
614
+ backoff = velocity.dot(normal)
615
+ Math::Vec3.new(
616
+ stop_epsilon_component(velocity.x - normal.x * backoff),
617
+ stop_epsilon_component(velocity.y - normal.y * backoff),
618
+ stop_epsilon_component(velocity.z - normal.z * backoff)
336
619
  )
620
+ end
621
+
622
+ def stop_epsilon_component(value)
623
+ value > -STOP_EPSILON && value < STOP_EPSILON ? 0.0 : value
624
+ end
337
625
 
338
- remaining = @velocity * (dt * (1.0 - trace_result.fraction))
339
- desired = @position + remaining
340
- result = trace_move(level, @position, desired)
341
- @position = result.end_pos
626
+ def apply_wall_friction(normal)
627
+ d = normal.dot(forward) + 0.5
628
+ return if d >= 0
629
+
630
+ into = normal * normal.dot(@velocity)
631
+ side = @velocity - into
632
+ @velocity = Math::Vec3.new(side.x * (1.0 + d), side.y * (1.0 + d), @velocity.z)
342
633
  end
343
634
 
344
635
  def noclip_move(dt, keys)
345
636
  wish = Math::Vec3::ORIGIN
346
- wish = wish + forward if keys[SDL::SCANCODE_W]
347
- wish = wish - forward if keys[SDL::SCANCODE_S]
348
- wish = wish + right if keys[SDL::SCANCODE_D]
349
- wish = wish - right if keys[SDL::SCANCODE_A]
350
- wish = wish + Math::Vec3.new(0.0, 0.0, 1.0) if keys[SDL::SCANCODE_SPACE]
351
- wish = wish - Math::Vec3.new(0.0, 0.0, 1.0) if keys[SDL::SCANCODE_C]
352
-
353
- @position = @position + wish * (MAX_SPEED * dt)
637
+ wish = wish + forward_flat * FORWARD_SPEED if keys[SDL::SCANCODE_W]
638
+ wish = wish - forward_flat * BACK_SPEED if keys[SDL::SCANCODE_S]
639
+ wish = wish + right_flat * SIDE_SPEED if keys[SDL::SCANCODE_D]
640
+ wish = wish - right_flat * SIDE_SPEED if keys[SDL::SCANCODE_A]
641
+ wish = wish + Math::Vec3.new(0.0, 0.0, UP_SPEED) if keys[SDL::SCANCODE_SPACE]
642
+ wish = wish - Math::Vec3.new(0.0, 0.0, UP_SPEED) if keys[SDL::SCANCODE_C]
643
+
644
+ wish_len = wish.length
645
+ if wish_len > MAX_SPEED
646
+ wish = wish * (MAX_SPEED / wish_len)
647
+ end
648
+
649
+ @velocity = wish
650
+ @position = @position + @velocity * dt
354
651
  end
355
652
  end
356
653
  end