Module: Quake::Physics::HullTrace

Defined in:
lib/quake/physics/hull_trace.rb

Class Method Summary collapse

Class Method Details

.point_contents(clipnodes, planes, node_index, point) ⇒ Object

Check what content type a point is in, using the clipnode tree. hull_clipnodes: array of ClipNode planes: array of Plane node_index: starting clipnode index point: Vec3



31
32
33
34
35
36
37
38
39
40
# File 'lib/quake/physics/hull_trace.rb', line 31

def self.point_contents(clipnodes, planes, node_index, point)
  while node_index >= 0
    node = clipnodes[node_index]
    plane = planes[node.plane_index]

    dist = point.dot(plane.normal) - plane.dist
    node_index = dist >= 0 ? node.children[0] : node.children[1]
  end
  node_index # negative value = content type
end

.trace(clipnodes, planes, node_index, p1, p2) ⇒ Object

Trace a line from p1 to p2 through the hull. Returns a TraceResult.



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/quake/physics/hull_trace.rb', line 44

def self.trace(clipnodes, planes, node_index, p1, p2)
  result = {
    all_solid: false, start_solid: false,
    in_open: false, in_water: false,
    fraction: 1.0, end_pos: p2,
    plane_normal: nil, plane_dist: nil
  }

  recursive_trace(clipnodes, planes, node_index, 0.0, 1.0, p1, p2, result)

  if result[:fraction] == 1.0
    result[:end_pos] = p2
  else
    result[:end_pos] = Math::Vec3.new(
      p1.x + result[:fraction] * (p2.x - p1.x),
      p1.y + result[:fraction] * (p2.y - p1.y),
      p1.z + result[:fraction] * (p2.z - p1.z)
    )
  end

  TraceResult.new(**result)
end

.trace_world_and_entities(level, p1, p2, brush_entities, hull_num: 1) ⇒ Object

Trace against the world AND all solid brush entities, returning the nearest hit. Brush entity traces are done in entity-local space (subtract origin, trace sub-model hull, transform back).



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
# File 'lib/quake/physics/hull_trace.rb', line 70

def self.trace_world_and_entities(level, p1, p2, brush_entities, hull_num: 1)
  clipnodes = level.clipnodes
  planes = level.planes

  # World trace
  world_hull = level.models[0].head_nodes[hull_num]
  world_hull = level.models[0].head_nodes[0] if world_hull < 0 || world_hull >= clipnodes.size
  best = trace(clipnodes, planes, world_hull, p1, p2)

  # Trace against each brush entity's sub-model
  brush_entities&.each do |ent|
    next unless ent.brush_entity?
    # Triggers are SOLID_TRIGGER in Quake: they detect overlap but
    # don't block movement. They're handled by check_triggers.
    next if ent.classname.start_with?("trigger_")
    model = level.models[ent.model_index]
    next unless model

    sub_hull = model.head_nodes[hull_num]
    # Many sub-models only have hull 0; fall back gracefully
    sub_hull = model.head_nodes[0] if sub_hull < 0 || sub_hull >= clipnodes.size
    next if sub_hull < 0 || sub_hull >= clipnodes.size

    # Transform trace into entity-local space
    offset = ent.position
    local_p1 = Math::Vec3.new(p1.x - offset.x, p1.y - offset.y, p1.z - offset.z)
    local_p2 = Math::Vec3.new(p2.x - offset.x, p2.y - offset.y, p2.z - offset.z)

    sub_result = trace(clipnodes, planes, sub_hull, local_p1, local_p2)

    # Keep nearest hit
    if sub_result.fraction < best.fraction
      # Transform end_pos back to world space
      ep = sub_result.end_pos
      best = TraceResult.new(
        all_solid: sub_result.all_solid,
        start_solid: sub_result.start_solid,
        in_open: sub_result.in_open,
        in_water: sub_result.in_water,
        fraction: sub_result.fraction,
        end_pos: Math::Vec3.new(ep.x + offset.x, ep.y + offset.y, ep.z + offset.z),
        plane_normal: sub_result.plane_normal,
        plane_dist: sub_result.plane_dist
      )
    end
  end

  best
end