Class: Quake::Game::Engine

Inherits:
Object
  • Object
show all
Defined in:
lib/quake/game/engine.rb

Overview

Encapsulates the entire game state and per-frame loop, allowing both interactive (bin/quake) and scripted (bin/quake-debug) execution.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(pak_path:, window: nil, window_visible: true, window_width: nil, window_height: nil, enable_sound: true, enable_render: true) ⇒ Engine

Returns a new instance of Engine.



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
# File 'lib/quake/game/engine.rb', line 16

def initialize(pak_path:, window: nil, window_visible: true,
               window_width: nil, window_height: nil,
               enable_sound: true, enable_render: true)
  @enable_render = enable_render

  # PAK + assets
  @pak = Pak::Reader.new(pak_path)
  @palette = Palette.new(@pak.read("gfx/palette.lmp"))

  wad_data = @pak.read("gfx.wad")
  @wad = Wad::Reader.new(wad_data) if wad_data

  # Sound (optional)
  if enable_sound
    @sound_mixer = Sound::Mixer.new(@pak)
    @sound_mixer.open
  end
  @sound_events = Sound::Events.new(@sound_mixer)

  # Window
  if window
    @window = window
    @owns_window = false
  else
    opts = { visible: window_visible }
    opts[:width] = window_width if window_width
    opts[:height] = window_height if window_height
    @window = Window.new(**opts)
    @owns_window = true
  end

  @keys = {}
  @game_time = 0.0
end

Instance Attribute Details

#brush_gameObject (readonly)

Returns the value of attribute brush_game.



10
11
12
# File 'lib/quake/game/engine.rb', line 10

def brush_game
  @brush_game
end

#cameraObject (readonly)

Returns the value of attribute camera.



10
11
12
# File 'lib/quake/game/engine.rb', line 10

def camera
  @camera
end

#entitiesObject (readonly)

Returns the value of attribute entities.



10
11
12
# File 'lib/quake/game/engine.rb', line 10

def entities
  @entities
end

#game_timeObject

Returns the value of attribute game_time.



14
15
16
# File 'lib/quake/game/engine.rb', line 14

def game_time
  @game_time
end

#hudObject (readonly)

Returns the value of attribute hud.



10
11
12
# File 'lib/quake/game/engine.rb', line 10

def hud
  @hud
end

#item_pickupsObject (readonly)

Returns the value of attribute item_pickups.



10
11
12
# File 'lib/quake/game/engine.rb', line 10

def item_pickups
  @item_pickups
end

#levelObject (readonly)

Returns the value of attribute level.



10
11
12
# File 'lib/quake/game/engine.rb', line 10

def level
  @level
end

#pakObject (readonly)

Returns the value of attribute pak.



10
11
12
# File 'lib/quake/game/engine.rb', line 10

def pak
  @pak
end

#paletteObject (readonly)

Returns the value of attribute palette.



10
11
12
# File 'lib/quake/game/engine.rb', line 10

def palette
  @palette
end

#particlesObject (readonly)

Returns the value of attribute particles.



10
11
12
# File 'lib/quake/game/engine.rb', line 10

def particles
  @particles
end

#playerObject (readonly)

Returns the value of attribute player.



10
11
12
# File 'lib/quake/game/engine.rb', line 10

def player
  @player
end

#player_stateObject (readonly)

Returns the value of attribute player_state.



10
11
12
# File 'lib/quake/game/engine.rb', line 10

def player_state
  @player_state
end

#sound_eventsObject (readonly)

Returns the value of attribute sound_events.



10
11
12
# File 'lib/quake/game/engine.rb', line 10

def sound_events
  @sound_events
end

#viewmodelObject (readonly)

Returns the value of attribute viewmodel.



10
11
12
# File 'lib/quake/game/engine.rb', line 10

def viewmodel
  @viewmodel
end

#wadObject (readonly)

Returns the value of attribute wad.



10
11
12
# File 'lib/quake/game/engine.rb', line 10

def wad
  @wad
end

#windowObject (readonly)

Returns the value of attribute window.



10
11
12
# File 'lib/quake/game/engine.rb', line 10

def window
  @window
end

Instance Method Details

#clear_keysObject



186
187
188
# File 'lib/quake/game/engine.rb', line 186

def clear_keys
  @keys = {}
end

#dump_stateObject



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/quake/game/engine.rb', line 212

def dump_state
  {
    time: @game_time,
    player: {
      position: vec_to_a(@player.position),
      velocity: vec_to_a(@player.velocity),
      yaw: @player.yaw,
      pitch: @player.pitch,
      on_ground: @player.on_ground,
      water_level: @player.water_level,
      noclip: @player.noclip
    },
    stats: {
      health: @player_state.health,
      armor: @player_state.armor,
      armor_type: @player_state.armor_type,
      current_weapon: @player_state.current_weapon,
      ammo: @player_state.ammo.dup
    },
    brush_entities: brush_entity_summaries,
    particles: @particles ? @particles.particle_count : 0
  }
end

#keysObject



190
191
192
# File 'lib/quake/game/engine.rb', line 190

def keys
  @keys
end

#load_map(map_name) ⇒ Object

Load a BSP map and (re)build all per-level state.



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
# File 'lib/quake/game/engine.rb', line 52

def load_map(map_name)
  bsp_data = @pak.read(map_name)
  raise "Map not found: #{map_name}" unless bsp_data

  @level = Bsp::Reader.new(bsp_data).parse
  @entities = EntityParser.parse(@level.entities)
  @target_map = EntityParser.build_target_map(@entities)

  # Find player start
  player_ent = @entities.find { |e| e.classname == "info_player_start" }
  @player_start = player_ent&.position || Math::Vec3::ORIGIN
  @player_yaw = player_ent&.angle || 0.0

  # Load MDL models referenced by entities
  @mdl_cache ||= {}
  @entities.each do |ent|
    model_path = ent["model"]
    next unless model_path&.end_with?(".mdl")
    next if @mdl_cache.key?(model_path)
    begin
      mdl_data = @pak.read(model_path)
      @mdl_cache[model_path] = Mdl::Reader.new(mdl_data).parse
    rescue => e
      warn "Failed to load #{model_path}: #{e.message}"
      @mdl_cache[model_path] = nil
    end
  end

  # Persistent player state across level loads
  @player_state ||= PlayerState.new
  @item_pickups = ItemPickups.new(@entities)
  @brush_game = BrushEntities.new(@entities, @level, @target_map)

  # Player + camera (reset position to start)
  @player = Physics::Player.new(position: @player_start, yaw: @player_yaw)
  @camera = Camera.new(position: @player.eye_position, yaw: @player_yaw)

  build_renderers if @enable_render
end

#renderObject



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
# File 'lib/quake/game/engine.rb', line 143

def render
  @world_renderer.render(@camera, @window.aspect_ratio)
  @brush_renderer&.render(@entities)

  @entities.each do |ent|
    next if @item_pickups.picked_up?(ent)
    model_path = ent["model"]
    next unless model_path&.end_with?(".mdl")

    gl_model = @mdl_renderers[model_path]
    next unless gl_model

    frame_rate = 10.0
    frame = (@game_time * frame_rate).to_i
    lerp = (@game_time * frame_rate) % 1.0

    gl_model.render(
      frame_index: frame,
      lerp: lerp,
      position: ent.position,
      yaw: ent.angle
    )
  end

  @particles&.render
  @viewmodel&.render(@camera, @window.aspect_ratio)
  @hud&.render(@player_state)
end

#screenshot(filename) ⇒ Object

———————- debug output ———————–



208
209
210
# File 'lib/quake/game/engine.rb', line 208

def screenshot(filename)
  Debug::Screenshot.save(filename, @window.width, @window.height)
end

#set_key(scancode, pressed) ⇒ Object

———————- input control ———————-



178
179
180
# File 'lib/quake/game/engine.rb', line 178

def set_key(scancode, pressed)
  @keys[scancode] = pressed
end

#set_keys(keys_hash) ⇒ Object



182
183
184
# File 'lib/quake/game/engine.rb', line 182

def set_keys(keys_hash)
  @keys = keys_hash.dup
end

#shutdownObject



236
237
238
239
240
241
242
# File 'lib/quake/game/engine.rb', line 236

def shutdown
  @sound_mixer&.close
  if @owns_window
    @window&.close
  end
  @pak.close
end

#swap_buffersObject



172
173
174
# File 'lib/quake/game/engine.rb', line 172

def swap_buffers
  @window.swap
end

#teleport(x, y, z, yaw: nil, pitch: nil) ⇒ Object

———————- player control ———————



196
197
198
199
200
201
202
203
204
# File 'lib/quake/game/engine.rb', line 196

def teleport(x, y, z, yaw: nil, pitch: nil)
  @player.position = Math::Vec3.new(x.to_f, y.to_f, z.to_f)
  @player.velocity = Math::Vec3::ORIGIN
  @player.instance_variable_set(:@yaw, yaw.to_f) if yaw
  @player.instance_variable_set(:@pitch, pitch.to_f) if pitch
  @camera.position = @player.eye_position
  @camera.yaw = @player.yaw
  @camera.pitch = @player.pitch
end

#tick(dt) ⇒ Object

Update + render one frame.



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
# File 'lib/quake/game/engine.rb', line 93

def tick(dt)
  @game_time += dt

  # Brush entities first (so collision uses current positions)
  if @brush_game
    @brush_game.update(dt, @player.position)
    @brush_game.check_triggers(@player.position) do |trigger|
      handle_trigger(trigger)
    end

    snapped = @brush_game.snap_to_platform(@player.position)
    if snapped
      @player.position = snapped
      @player.on_ground = true
      @player.velocity = Math::Vec3.new(@player.velocity.x, @player.velocity.y, 0.0)
    end
  end

  solid_brush = @entities.select(&:brush_entity?)
  @player.update(dt, @level, @keys, brush_entities: solid_brush)

  # Item pickups
  events = @item_pickups.check_pickups(@player.position, @player_state)
  events.each do |evt|
    @sound_events&.on_pickup(evt)
    @particles&.pickup_effect(evt[:entity].position)
    @viewmodel&.set_weapon(@player_state.current_weapon_model)
  end

  # Viewmodel bob (before camera so bob is applied to camera too)
  if @viewmodel
    speed = ::Math.sqrt(@player.velocity.x**2 + @player.velocity.y**2)
    @viewmodel.update(dt, speed)
  end

  # Sync camera
  eye = @player.eye_position
  bob = @viewmodel ? @viewmodel.bob : 0.0
  @camera.position = Math::Vec3.new(eye.x, eye.y, eye.z + bob)
  @camera.yaw = @player.yaw
  @camera.pitch = @player.pitch

  return unless @enable_render

  @world_renderer.update(dt) if @world_renderer.respond_to?(:update)
  @particles&.update(dt)

  render
end