doom 0.8.0 → 0.11.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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +17 -0
  3. data/README.md +65 -4
  4. data/bin/doom +134 -24
  5. data/lib/doom/benchmark.rb +282 -0
  6. data/lib/doom/game/animations.rb +9 -2
  7. data/lib/doom/game/combat.rb +239 -153
  8. data/lib/doom/game/framebuffer_blitter.rb +38 -0
  9. data/lib/doom/game/geometry.rb +68 -0
  10. data/lib/doom/game/intermission.rb +22 -39
  11. data/lib/doom/game/item_pickup.rb +87 -75
  12. data/lib/doom/game/menu.rb +90 -85
  13. data/lib/doom/game/monster_ai.rb +150 -136
  14. data/lib/doom/game/player.rb +105 -0
  15. data/lib/doom/game/player_physics.rb +384 -0
  16. data/lib/doom/game/player_state.rb +38 -64
  17. data/lib/doom/game/random.rb +82 -0
  18. data/lib/doom/game/sector_actions.rb +169 -93
  19. data/lib/doom/game/sector_effects.rb +25 -18
  20. data/lib/doom/game/snapshot.rb +584 -0
  21. data/lib/doom/game/sound_engine.rb +33 -58
  22. data/lib/doom/game/state_hash.rb +125 -0
  23. data/lib/doom/game/ticcmd.rb +61 -0
  24. data/lib/doom/game/world.rb +420 -0
  25. data/lib/doom/map/data.rb +95 -0
  26. data/lib/doom/net/client.rb +204 -0
  27. data/lib/doom/net/desync_monitor.rb +101 -0
  28. data/lib/doom/net/game_server.rb +232 -0
  29. data/lib/doom/net/lockstep.rb +170 -0
  30. data/lib/doom/net/protocol.rb +350 -0
  31. data/lib/doom/net/session.rb +282 -0
  32. data/lib/doom/net/transport.rb +110 -0
  33. data/lib/doom/platform/gosu_window.rb +604 -832
  34. data/lib/doom/platform/sdl.rb +74 -0
  35. data/lib/doom/platform/window_logic.rb +61 -0
  36. data/lib/doom/render/font.rb +5 -3
  37. data/lib/doom/render/hardware_renderer.rb +547 -0
  38. data/lib/doom/render/ray_tracing/bvh.rb +103 -0
  39. data/lib/doom/render/ray_tracing/material_state.rb +66 -0
  40. data/lib/doom/render/ray_tracing/texture_atlas.rb +78 -0
  41. data/lib/doom/render/ray_tracing_renderer.rb +940 -0
  42. data/lib/doom/render/renderer.rb +466 -440
  43. data/lib/doom/render/renderer_factory.rb +50 -0
  44. data/lib/doom/render/screen_melt.rb +7 -7
  45. data/lib/doom/render/spinel_native/kernel.rb +780 -0
  46. data/lib/doom/render/spinel_native_renderer.rb +162 -0
  47. data/lib/doom/render/status_bar.rb +14 -10
  48. data/lib/doom/render/weapon_renderer.rb +9 -10
  49. data/lib/doom/render/world_mesh.rb +270 -0
  50. data/lib/doom/render/zbuffer_renderer.rb +151 -0
  51. data/lib/doom/version.rb +1 -1
  52. data/lib/doom/wad/flat.rb +1 -1
  53. data/lib/doom/wad/hud_graphics.rb +7 -3
  54. data/lib/doom/wad/palette.rb +3 -3
  55. data/lib/doom/wad/patch.rb +1 -1
  56. data/lib/doom/wad/reader.rb +12 -15
  57. data/lib/doom/wad/sound.rb +14 -6
  58. data/lib/doom/wad/sprite.rb +36 -35
  59. data/lib/doom/wad/texture.rb +5 -5
  60. data/lib/doom/wad_downloader.rb +38 -56
  61. data/lib/doom.rb +197 -16
  62. metadata +94 -7
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'framebuffer_blitter'
4
+
3
5
  module Doom
4
6
  module Game
5
7
  # DOOM main menu system with title screen, new game, and difficulty selection.
6
8
  class Menu
7
- SKULL_ANIM_TICS = 8 # Skull cursor blink rate
9
+ include FramebufferBlitter
10
+
11
+ SKULL_ANIM_TICS = 8 # Skull cursor blink rate
8
12
 
9
13
  # Difficulty levels matching DOOM's skill levels
10
14
  SKILL_BABY = 0 # I'm too young to die
@@ -18,24 +22,37 @@ module Doom
18
22
  STATE_MAIN = :main
19
23
  STATE_SKILL = :skill
20
24
  STATE_OPTIONS = :options
21
- STATE_NONE = :none # In-game, no menu
25
+ STATE_NONE = :none # In-game, no menu
22
26
 
23
27
  # Main menu items
24
28
  MAIN_ITEMS = %i[new_game options quit].freeze
25
29
 
30
+ # Menu entries that change the simulation on this machine alone. In a
31
+ # netgame every peer must run the same world from the same commands, so
32
+ # a local cheat or a local new game is a genuine desync, not a nuisance.
33
+ # They are hidden rather than ignored: an option that silently does
34
+ # nothing is worse than one that is not offered.
35
+ NETGAME_UNSAFE_OPTIONS = %i[god_mode infinite_ammo all_weapons].freeze
36
+ NETGAME_UNSAFE_MAIN = %i[new_game].freeze
37
+
26
38
  # Options menu items
27
- OPTIONS_ITEMS = %i[god_mode infinite_ammo all_weapons fullscreen rubykaigi_mode].freeze
39
+ OPTIONS_ITEMS = %i[god_mode infinite_ammo all_weapons uncapped_fps fullscreen fog flashlight rt_bounces
40
+ rubykaigi_mode].freeze
28
41
  OPTIONS_LABELS = {
29
- god_mode: "GOD MODE",
30
- infinite_ammo: "INFINITE AMMO",
31
- all_weapons: "ALL WEAPONS",
32
- fullscreen: "FULLSCREEN",
33
- rubykaigi_mode: "RUBYKAIGI MODE",
42
+ god_mode: 'GOD MODE',
43
+ infinite_ammo: 'INFINITE AMMO',
44
+ all_weapons: 'ALL WEAPONS',
45
+ uncapped_fps: 'UNCAPPED FPS',
46
+ fullscreen: 'FULLSCREEN',
47
+ fog: 'RAY FOG',
48
+ flashlight: 'FLASHLIGHT',
49
+ rt_bounces: 'RT BOUNCES',
50
+ rubykaigi_mode: 'RUBYKAIGI MODE'
34
51
  }.freeze
35
52
 
36
53
  OPTIONS_X = 48
37
54
  OPTIONS_Y = 50
38
- OPTIONS_SPACING = 18
55
+ OPTIONS_SPACING = 16
39
56
 
40
57
  # Skill menu items
41
58
  SKILL_ITEMS = [SKILL_BABY, SKILL_EASY, SKILL_MEDIUM, SKILL_HARD, SKILL_NIGHTMARE].freeze
@@ -50,25 +67,30 @@ module Doom
50
67
  SKILL_SPACING = 16
51
68
 
52
69
  attr_reader :state, :selected_skill, :options, :font
70
+ attr_accessor :netgame
53
71
 
54
- def initialize(wad, hud_graphics, font = nil)
55
- @wad = wad
72
+ def initialize(_wad, hud_graphics, font = nil)
56
73
  @gfx = hud_graphics
57
74
  @font = font
58
75
  @state = STATE_TITLE
59
76
  @cursor = 0
60
77
  @skull_frame = 0
61
78
  @skull_tic = 0
62
- @selected_skill = SKILL_MEDIUM # Default difficulty
79
+ @selected_skill = SKILL_MEDIUM # Default difficulty
63
80
  @game_started = false
81
+ @netgame = false
64
82
 
65
83
  # Options toggles
66
84
  @options = {
67
85
  god_mode: false,
68
86
  infinite_ammo: false,
69
87
  all_weapons: false,
88
+ uncapped_fps: true,
70
89
  fullscreen: false,
71
- rubykaigi_mode: false,
90
+ fog: true,
91
+ flashlight: true,
92
+ rt_bounces: true,
93
+ rubykaigi_mode: false
72
94
  }
73
95
 
74
96
  load_graphics
@@ -84,13 +106,15 @@ module Doom
84
106
 
85
107
  def update
86
108
  @skull_tic += 1
87
- if @skull_tic >= SKULL_ANIM_TICS
88
- @skull_tic = 0
89
- @skull_frame = 1 - @skull_frame
90
- end
109
+ return unless @skull_tic >= SKULL_ANIM_TICS
110
+
111
+ @skull_tic = 0
112
+ @skull_frame = 1 - @skull_frame
91
113
  end
92
114
 
93
- def render(framebuffer, palette_colors)
115
+ # NOTE: palette_colors is unused here (callers in gosu_window.rb pass nil).
116
+ # Kept only to preserve the call signature; drop it once the callers do.
117
+ def render(framebuffer, _palette_colors)
94
118
  case @state
95
119
  when STATE_TITLE
96
120
  render_title(framebuffer)
@@ -104,6 +128,14 @@ module Doom
104
128
  end
105
129
 
106
130
  # Returns :start_game, :resume, :quit, or option action symbols
131
+ def main_items
132
+ @netgame ? MAIN_ITEMS - NETGAME_UNSAFE_MAIN : MAIN_ITEMS
133
+ end
134
+
135
+ def options_items
136
+ @netgame ? OPTIONS_ITEMS - NETGAME_UNSAFE_OPTIONS : OPTIONS_ITEMS
137
+ end
138
+
107
139
  def handle_key(key)
108
140
  case @state
109
141
  when STATE_TITLE
@@ -148,29 +180,26 @@ module Doom
148
180
  @m_skill = load_patch('M_SKILL')
149
181
  @m_jkill = load_patch('M_JKILL')
150
182
  @m_hurt = load_patch('M_HURT')
151
- @m_rough = load_patch('M_ROUGH') # Not used, but loaded
183
+ @m_rough = load_patch('M_ROUGH')
152
184
  @m_ultra = load_patch('M_ULTRA')
153
185
  @m_nmare = load_patch('M_NMARE')
154
186
 
155
- # Episode (shareware only has 1)
156
- @m_episod = load_patch('M_EPISOD')
157
- @m_epi1 = load_patch('M_EPI1')
158
-
159
187
  # Skull cursor
160
188
  @skulls = [load_patch('M_SKULL1'), load_patch('M_SKULL2')]
161
189
  end
162
190
 
163
191
  def load_patch(name)
164
- @gfx.send(:load_graphic, name)
192
+ @gfx.load_graphic(name)
165
193
  end
166
194
 
167
195
  def render_title(framebuffer)
168
196
  if @options[:rubykaigi_mode] && @kaigi_title
169
197
  # Draw kaigi title (raw palette indices, 320x200, offset 20px down)
198
+ w = Render::SCREEN_WIDTH
170
199
  @kaigi_title.each_with_index do |color, i|
171
- x = i % 320
172
- y = (i / 320) + 20
173
- framebuffer[y * 320 + x] = color if y < 240
200
+ x = i % w
201
+ y = (i / w) + 20
202
+ framebuffer[(y * w) + x] = color if y < Render::SCREEN_HEIGHT
174
203
  end
175
204
  elsif @title
176
205
  draw_fullscreen(framebuffer, @title)
@@ -185,16 +214,17 @@ module Doom
185
214
  items = [@m_newg, @m_option, @m_quitg]
186
215
  items.each_with_index do |item, i|
187
216
  next unless item
188
- draw_sprite(framebuffer, item, MAIN_X, MAIN_Y + i * MAIN_SPACING)
217
+
218
+ draw_sprite(framebuffer, item, MAIN_X, MAIN_Y + (i * MAIN_SPACING))
189
219
  end
190
220
 
191
221
  # Draw skull cursor
192
222
  skull = @skulls[@skull_frame]
193
- if skull
194
- skull_x = MAIN_X - 32
195
- skull_y = MAIN_Y + @cursor * MAIN_SPACING - 5
196
- draw_sprite(framebuffer, skull, skull_x, skull_y)
197
- end
223
+ return unless skull
224
+
225
+ skull_x = MAIN_X - 32
226
+ skull_y = MAIN_Y + (@cursor * MAIN_SPACING) - 5
227
+ draw_sprite(framebuffer, skull, skull_x, skull_y)
198
228
  end
199
229
 
200
230
  def render_skill_menu(framebuffer)
@@ -205,29 +235,30 @@ module Doom
205
235
  skill_items = [@m_jkill, @m_hurt, @m_rough, @m_ultra, @m_nmare]
206
236
  skill_items.each_with_index do |item, i|
207
237
  next unless item
208
- draw_sprite(framebuffer, item, SKILL_X, SKILL_Y + i * SKILL_SPACING)
238
+
239
+ draw_sprite(framebuffer, item, SKILL_X, SKILL_Y + (i * SKILL_SPACING))
209
240
  end
210
241
 
211
242
  # Draw skull cursor
212
243
  skull = @skulls[@skull_frame]
213
- if skull
214
- skull_x = SKILL_X - 32
215
- skull_y = SKILL_Y + @cursor * SKILL_SPACING - 5
216
- draw_sprite(framebuffer, skull, skull_x, skull_y)
217
- end
244
+ return unless skull
245
+
246
+ skull_x = SKILL_X - 32
247
+ skull_y = SKILL_Y + (@cursor * SKILL_SPACING) - 5
248
+ draw_sprite(framebuffer, skull, skull_x, skull_y)
218
249
  end
219
250
 
220
251
  def handle_main_key(key)
221
252
  case key
222
253
  when :up
223
- @cursor = (@cursor - 1) % MAIN_ITEMS.size
254
+ @cursor = (@cursor - 1) % main_items.size
224
255
  when :down
225
- @cursor = (@cursor + 1) % MAIN_ITEMS.size
256
+ @cursor = (@cursor + 1) % main_items.size
226
257
  when :enter
227
- case MAIN_ITEMS[@cursor]
258
+ case main_items[@cursor]
228
259
  when :new_game
229
260
  @state = STATE_SKILL
230
- @cursor = SKILL_MEDIUM # Default to "Hurt me plenty"
261
+ @cursor = SKILL_MEDIUM # Default to "Hurt me plenty"
231
262
  when :options
232
263
  @state = STATE_OPTIONS
233
264
  @cursor = 0
@@ -266,76 +297,50 @@ module Doom
266
297
 
267
298
  def render_options_menu(framebuffer)
268
299
  # Draw title using font
269
- @font&.draw_centered(framebuffer, "OPTIONS", 20)
300
+ @font&.draw_centered(framebuffer, 'OPTIONS', 20)
270
301
 
271
302
  # Draw each option with ON/OFF status
272
- OPTIONS_ITEMS.each_with_index do |item, i|
273
- y = OPTIONS_Y + i * OPTIONS_SPACING
303
+ options_items.each_with_index do |item, i|
304
+ y = OPTIONS_Y + (i * OPTIONS_SPACING)
274
305
  label = OPTIONS_LABELS[item]
275
- value = @options[item] ? "ON" : "OFF"
306
+ value = @options[item] ? 'ON' : 'OFF'
276
307
  @font&.draw_text(framebuffer, label, OPTIONS_X, y)
277
308
  @font&.draw_text(framebuffer, value, 260, y)
278
309
  end
279
310
 
280
311
  # Draw skull cursor
281
312
  skull = @skulls[@skull_frame]
282
- if skull
283
- skull_x = OPTIONS_X - 32
284
- skull_y = OPTIONS_Y + @cursor * OPTIONS_SPACING - 5
285
- draw_sprite(framebuffer, skull, skull_x, skull_y)
286
- end
313
+ return unless skull
314
+
315
+ skull_x = OPTIONS_X - 32
316
+ skull_y = OPTIONS_Y + (@cursor * OPTIONS_SPACING) - 5
317
+ draw_sprite(framebuffer, skull, skull_x, skull_y)
287
318
  end
288
319
 
289
320
  def handle_options_key(key)
290
321
  case key
291
322
  when :up
292
- @cursor = (@cursor - 1) % OPTIONS_ITEMS.size
323
+ @cursor = (@cursor - 1) % options_items.size
293
324
  when :down
294
- @cursor = (@cursor + 1) % OPTIONS_ITEMS.size
325
+ @cursor = (@cursor + 1) % options_items.size
295
326
  when :enter
296
- item = OPTIONS_ITEMS[@cursor]
327
+ item = options_items[@cursor]
297
328
  @options[item] = !@options[item]
298
329
  return { action: :toggle_option, option: item, value: @options[item] }
299
330
  when :escape
300
331
  @state = STATE_MAIN
301
- @cursor = 1 # Options is the second main menu item
332
+ @cursor = 1 # Options is the second main menu item
302
333
  end
303
334
  nil
304
335
  end
305
336
 
306
337
  def draw_fullscreen(framebuffer, sprite)
307
- return unless sprite
308
- # TITLEPIC is 320x200, our screen is 320x240
309
- # Draw it centered vertically (offset by 20 pixels)
310
- y_offset = 20
311
- sprite.width.times do |x|
312
- col = sprite.column_pixels(x)
313
- next unless col
314
- col.each_with_index do |color, y|
315
- next unless color
316
- screen_y = y + y_offset
317
- next if screen_y < 0 || screen_y >= 240
318
- framebuffer[screen_y * 320 + x] = color
319
- end
320
- end
338
+ # TITLEPIC is 320x200, our screen is 320x240: center it vertically.
339
+ blit_sprite(framebuffer, sprite, 0, 20)
321
340
  end
322
341
 
323
342
  def draw_sprite(framebuffer, sprite, x, y)
324
- return unless sprite
325
- sprite.width.times do |col_x|
326
- screen_x = x + col_x
327
- next if screen_x < 0 || screen_x >= 320
328
-
329
- col = sprite.column_pixels(col_x)
330
- next unless col
331
-
332
- col.each_with_index do |color, col_y|
333
- next unless color
334
- screen_y = y + col_y
335
- next if screen_y < 0 || screen_y >= 240
336
- framebuffer[screen_y * 320 + screen_x] = color
337
- end
338
- end
343
+ blit_sprite(framebuffer, sprite, x, y)
339
344
  end
340
345
  end
341
346
  end