doom 0.6.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 +138 -25
  5. data/lib/doom/benchmark.rb +282 -0
  6. data/lib/doom/game/animations.rb +9 -2
  7. data/lib/doom/game/combat.rb +377 -105
  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 +231 -0
  11. data/lib/doom/game/item_pickup.rb +108 -60
  12. data/lib/doom/game/menu.rb +347 -0
  13. data/lib/doom/game/monster_ai.rb +304 -91
  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 +47 -64
  17. data/lib/doom/game/random.rb +82 -0
  18. data/lib/doom/game/sector_actions.rb +466 -29
  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 +176 -0
  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 +820 -640
  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 +80 -0
  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 +684 -330
  43. data/lib/doom/render/renderer_factory.rb +50 -0
  44. data/lib/doom/render/screen_melt.rb +71 -0
  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 +15 -17
  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 +59 -10
  57. data/lib/doom/wad/sound.rb +93 -0
  58. data/lib/doom/wad/sprite.rb +60 -34
  59. data/lib/doom/wad/texture.rb +5 -5
  60. data/lib/doom/wad_downloader.rb +38 -56
  61. data/lib/doom.rb +235 -12
  62. metadata +100 -7
@@ -0,0 +1,347 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'framebuffer_blitter'
4
+
5
+ module Doom
6
+ module Game
7
+ # DOOM main menu system with title screen, new game, and difficulty selection.
8
+ class Menu
9
+ include FramebufferBlitter
10
+
11
+ SKULL_ANIM_TICS = 8 # Skull cursor blink rate
12
+
13
+ # Difficulty levels matching DOOM's skill levels
14
+ SKILL_BABY = 0 # I'm too young to die
15
+ SKILL_EASY = 1 # Hey, not too rough
16
+ SKILL_MEDIUM = 2 # Hurt me plenty
17
+ SKILL_HARD = 3 # Ultra-Violence
18
+ SKILL_NIGHTMARE = 4 # Nightmare!
19
+
20
+ # Menu states
21
+ STATE_TITLE = :title
22
+ STATE_MAIN = :main
23
+ STATE_SKILL = :skill
24
+ STATE_OPTIONS = :options
25
+ STATE_NONE = :none # In-game, no menu
26
+
27
+ # Main menu items
28
+ MAIN_ITEMS = %i[new_game options quit].freeze
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
+
38
+ # Options menu items
39
+ OPTIONS_ITEMS = %i[god_mode infinite_ammo all_weapons uncapped_fps fullscreen fog flashlight rt_bounces
40
+ rubykaigi_mode].freeze
41
+ OPTIONS_LABELS = {
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'
51
+ }.freeze
52
+
53
+ OPTIONS_X = 48
54
+ OPTIONS_Y = 50
55
+ OPTIONS_SPACING = 16
56
+
57
+ # Skill menu items
58
+ SKILL_ITEMS = [SKILL_BABY, SKILL_EASY, SKILL_MEDIUM, SKILL_HARD, SKILL_NIGHTMARE].freeze
59
+
60
+ # Menu item Y positions (from Chocolate Doom m_menu.c)
61
+ MAIN_X = 97
62
+ MAIN_Y = 64
63
+ MAIN_SPACING = 16
64
+
65
+ SKILL_X = 48
66
+ SKILL_Y = 63
67
+ SKILL_SPACING = 16
68
+
69
+ attr_reader :state, :selected_skill, :options, :font
70
+ attr_accessor :netgame
71
+
72
+ def initialize(_wad, hud_graphics, font = nil)
73
+ @gfx = hud_graphics
74
+ @font = font
75
+ @state = STATE_TITLE
76
+ @cursor = 0
77
+ @skull_frame = 0
78
+ @skull_tic = 0
79
+ @selected_skill = SKILL_MEDIUM # Default difficulty
80
+ @game_started = false
81
+ @netgame = false
82
+
83
+ # Options toggles
84
+ @options = {
85
+ god_mode: false,
86
+ infinite_ammo: false,
87
+ all_weapons: false,
88
+ uncapped_fps: true,
89
+ fullscreen: false,
90
+ fog: true,
91
+ flashlight: true,
92
+ rt_bounces: true,
93
+ rubykaigi_mode: false
94
+ }
95
+
96
+ load_graphics
97
+ end
98
+
99
+ def active?
100
+ @state != STATE_NONE
101
+ end
102
+
103
+ def needs_background?
104
+ @state != STATE_TITLE
105
+ end
106
+
107
+ def update
108
+ @skull_tic += 1
109
+ return unless @skull_tic >= SKULL_ANIM_TICS
110
+
111
+ @skull_tic = 0
112
+ @skull_frame = 1 - @skull_frame
113
+ end
114
+
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)
118
+ case @state
119
+ when STATE_TITLE
120
+ render_title(framebuffer)
121
+ when STATE_MAIN
122
+ render_main_menu(framebuffer)
123
+ when STATE_SKILL
124
+ render_skill_menu(framebuffer)
125
+ when STATE_OPTIONS
126
+ render_options_menu(framebuffer)
127
+ end
128
+ end
129
+
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
+
139
+ def handle_key(key)
140
+ case @state
141
+ when STATE_TITLE
142
+ @state = STATE_MAIN
143
+ @cursor = 0
144
+ when STATE_MAIN
145
+ handle_main_key(key)
146
+ when STATE_SKILL
147
+ handle_skill_key(key)
148
+ when STATE_OPTIONS
149
+ handle_options_key(key)
150
+ end
151
+ end
152
+
153
+ def dismiss
154
+ @state = STATE_NONE
155
+ @game_started = true
156
+ end
157
+
158
+ def show
159
+ @state = STATE_MAIN
160
+ @cursor = 0
161
+ end
162
+
163
+ private
164
+
165
+ def load_graphics
166
+ # Title screen
167
+ @title = load_patch('TITLEPIC')
168
+
169
+ # RubyKaigi title screen (pre-rendered palette indices)
170
+ kaigi_path = File.join(File.expand_path('../../..', __dir__), 'assets', 'kaigi_title.dat')
171
+ @kaigi_title = File.exist?(kaigi_path) ? Marshal.load(File.binread(kaigi_path)) : nil
172
+
173
+ # Main menu
174
+ @m_doom = load_patch('M_DOOM')
175
+ @m_newg = load_patch('M_NGAME')
176
+ @m_option = load_patch('M_OPTION')
177
+ @m_quitg = load_patch('M_QUITG')
178
+
179
+ # Skill menu
180
+ @m_skill = load_patch('M_SKILL')
181
+ @m_jkill = load_patch('M_JKILL')
182
+ @m_hurt = load_patch('M_HURT')
183
+ @m_rough = load_patch('M_ROUGH')
184
+ @m_ultra = load_patch('M_ULTRA')
185
+ @m_nmare = load_patch('M_NMARE')
186
+
187
+ # Skull cursor
188
+ @skulls = [load_patch('M_SKULL1'), load_patch('M_SKULL2')]
189
+ end
190
+
191
+ def load_patch(name)
192
+ @gfx.load_graphic(name)
193
+ end
194
+
195
+ def render_title(framebuffer)
196
+ if @options[:rubykaigi_mode] && @kaigi_title
197
+ # Draw kaigi title (raw palette indices, 320x200, offset 20px down)
198
+ w = Render::SCREEN_WIDTH
199
+ @kaigi_title.each_with_index do |color, i|
200
+ x = i % w
201
+ y = (i / w) + 20
202
+ framebuffer[(y * w) + x] = color if y < Render::SCREEN_HEIGHT
203
+ end
204
+ elsif @title
205
+ draw_fullscreen(framebuffer, @title)
206
+ end
207
+ end
208
+
209
+ def render_main_menu(framebuffer)
210
+ # Draw title logo
211
+ draw_sprite(framebuffer, @m_doom, 94, 2) if @m_doom
212
+
213
+ # Draw menu items
214
+ items = [@m_newg, @m_option, @m_quitg]
215
+ items.each_with_index do |item, i|
216
+ next unless item
217
+
218
+ draw_sprite(framebuffer, item, MAIN_X, MAIN_Y + (i * MAIN_SPACING))
219
+ end
220
+
221
+ # Draw skull cursor
222
+ skull = @skulls[@skull_frame]
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)
228
+ end
229
+
230
+ def render_skill_menu(framebuffer)
231
+ # Draw skill title
232
+ draw_sprite(framebuffer, @m_skill, 38, 15) if @m_skill
233
+
234
+ # Draw skill items: baby, easy, medium, hard, nightmare
235
+ skill_items = [@m_jkill, @m_hurt, @m_rough, @m_ultra, @m_nmare]
236
+ skill_items.each_with_index do |item, i|
237
+ next unless item
238
+
239
+ draw_sprite(framebuffer, item, SKILL_X, SKILL_Y + (i * SKILL_SPACING))
240
+ end
241
+
242
+ # Draw skull cursor
243
+ skull = @skulls[@skull_frame]
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)
249
+ end
250
+
251
+ def handle_main_key(key)
252
+ case key
253
+ when :up
254
+ @cursor = (@cursor - 1) % main_items.size
255
+ when :down
256
+ @cursor = (@cursor + 1) % main_items.size
257
+ when :enter
258
+ case main_items[@cursor]
259
+ when :new_game
260
+ @state = STATE_SKILL
261
+ @cursor = SKILL_MEDIUM # Default to "Hurt me plenty"
262
+ when :options
263
+ @state = STATE_OPTIONS
264
+ @cursor = 0
265
+ when :quit
266
+ return :quit
267
+ end
268
+ when :escape
269
+ if @game_started
270
+ # Resume game
271
+ @state = STATE_NONE
272
+ return :resume
273
+ else
274
+ @state = STATE_TITLE
275
+ end
276
+ end
277
+ nil
278
+ end
279
+
280
+ def handle_skill_key(key)
281
+ case key
282
+ when :up
283
+ @cursor = (@cursor - 1) % SKILL_ITEMS.size
284
+ when :down
285
+ @cursor = (@cursor + 1) % SKILL_ITEMS.size
286
+ when :enter
287
+ @selected_skill = SKILL_ITEMS[@cursor]
288
+ @state = STATE_NONE
289
+ @game_started = true
290
+ return :start_game
291
+ when :escape
292
+ @state = STATE_MAIN
293
+ @cursor = 0
294
+ end
295
+ nil
296
+ end
297
+
298
+ def render_options_menu(framebuffer)
299
+ # Draw title using font
300
+ @font&.draw_centered(framebuffer, 'OPTIONS', 20)
301
+
302
+ # Draw each option with ON/OFF status
303
+ options_items.each_with_index do |item, i|
304
+ y = OPTIONS_Y + (i * OPTIONS_SPACING)
305
+ label = OPTIONS_LABELS[item]
306
+ value = @options[item] ? 'ON' : 'OFF'
307
+ @font&.draw_text(framebuffer, label, OPTIONS_X, y)
308
+ @font&.draw_text(framebuffer, value, 260, y)
309
+ end
310
+
311
+ # Draw skull cursor
312
+ skull = @skulls[@skull_frame]
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)
318
+ end
319
+
320
+ def handle_options_key(key)
321
+ case key
322
+ when :up
323
+ @cursor = (@cursor - 1) % options_items.size
324
+ when :down
325
+ @cursor = (@cursor + 1) % options_items.size
326
+ when :enter
327
+ item = options_items[@cursor]
328
+ @options[item] = !@options[item]
329
+ return { action: :toggle_option, option: item, value: @options[item] }
330
+ when :escape
331
+ @state = STATE_MAIN
332
+ @cursor = 1 # Options is the second main menu item
333
+ end
334
+ nil
335
+ end
336
+
337
+ def draw_fullscreen(framebuffer, sprite)
338
+ # TITLEPIC is 320x200, our screen is 320x240: center it vertically.
339
+ blit_sprite(framebuffer, sprite, 0, 20)
340
+ end
341
+
342
+ def draw_sprite(framebuffer, sprite, x, y)
343
+ blit_sprite(framebuffer, sprite, x, y)
344
+ end
345
+ end
346
+ end
347
+ end