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.
- checksums.yaml +4 -4
- data/LICENSE +17 -0
- data/README.md +65 -4
- data/bin/doom +134 -24
- data/lib/doom/benchmark.rb +282 -0
- data/lib/doom/game/animations.rb +9 -2
- data/lib/doom/game/combat.rb +239 -153
- data/lib/doom/game/framebuffer_blitter.rb +38 -0
- data/lib/doom/game/geometry.rb +68 -0
- data/lib/doom/game/intermission.rb +22 -39
- data/lib/doom/game/item_pickup.rb +87 -75
- data/lib/doom/game/menu.rb +90 -85
- data/lib/doom/game/monster_ai.rb +150 -136
- data/lib/doom/game/player.rb +105 -0
- data/lib/doom/game/player_physics.rb +384 -0
- data/lib/doom/game/player_state.rb +38 -64
- data/lib/doom/game/random.rb +82 -0
- data/lib/doom/game/sector_actions.rb +169 -93
- data/lib/doom/game/sector_effects.rb +25 -18
- data/lib/doom/game/snapshot.rb +584 -0
- data/lib/doom/game/sound_engine.rb +33 -58
- data/lib/doom/game/state_hash.rb +125 -0
- data/lib/doom/game/ticcmd.rb +61 -0
- data/lib/doom/game/world.rb +420 -0
- data/lib/doom/map/data.rb +95 -0
- data/lib/doom/net/client.rb +204 -0
- data/lib/doom/net/desync_monitor.rb +101 -0
- data/lib/doom/net/game_server.rb +232 -0
- data/lib/doom/net/lockstep.rb +170 -0
- data/lib/doom/net/protocol.rb +350 -0
- data/lib/doom/net/session.rb +282 -0
- data/lib/doom/net/transport.rb +110 -0
- data/lib/doom/platform/gosu_window.rb +604 -832
- data/lib/doom/platform/sdl.rb +74 -0
- data/lib/doom/platform/window_logic.rb +61 -0
- data/lib/doom/render/font.rb +5 -3
- data/lib/doom/render/hardware_renderer.rb +547 -0
- data/lib/doom/render/ray_tracing/bvh.rb +103 -0
- data/lib/doom/render/ray_tracing/material_state.rb +66 -0
- data/lib/doom/render/ray_tracing/texture_atlas.rb +78 -0
- data/lib/doom/render/ray_tracing_renderer.rb +940 -0
- data/lib/doom/render/renderer.rb +466 -440
- data/lib/doom/render/renderer_factory.rb +50 -0
- data/lib/doom/render/screen_melt.rb +7 -7
- data/lib/doom/render/spinel_native/kernel.rb +780 -0
- data/lib/doom/render/spinel_native_renderer.rb +162 -0
- data/lib/doom/render/status_bar.rb +14 -10
- data/lib/doom/render/weapon_renderer.rb +9 -10
- data/lib/doom/render/world_mesh.rb +270 -0
- data/lib/doom/render/zbuffer_renderer.rb +151 -0
- data/lib/doom/version.rb +1 -1
- data/lib/doom/wad/flat.rb +1 -1
- data/lib/doom/wad/hud_graphics.rb +7 -3
- data/lib/doom/wad/palette.rb +3 -3
- data/lib/doom/wad/patch.rb +1 -1
- data/lib/doom/wad/reader.rb +12 -15
- data/lib/doom/wad/sound.rb +14 -6
- data/lib/doom/wad/sprite.rb +36 -35
- data/lib/doom/wad/texture.rb +5 -5
- data/lib/doom/wad_downloader.rb +38 -56
- data/lib/doom.rb +197 -16
- metadata +94 -7
data/lib/doom/game/menu.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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:
|
|
30
|
-
infinite_ammo:
|
|
31
|
-
all_weapons:
|
|
32
|
-
|
|
33
|
-
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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')
|
|
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.
|
|
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 %
|
|
172
|
-
y = (i /
|
|
173
|
-
framebuffer[y *
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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) %
|
|
254
|
+
@cursor = (@cursor - 1) % main_items.size
|
|
224
255
|
when :down
|
|
225
|
-
@cursor = (@cursor + 1) %
|
|
256
|
+
@cursor = (@cursor + 1) % main_items.size
|
|
226
257
|
when :enter
|
|
227
|
-
case
|
|
258
|
+
case main_items[@cursor]
|
|
228
259
|
when :new_game
|
|
229
260
|
@state = STATE_SKILL
|
|
230
|
-
@cursor = SKILL_MEDIUM
|
|
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,
|
|
300
|
+
@font&.draw_centered(framebuffer, 'OPTIONS', 20)
|
|
270
301
|
|
|
271
302
|
# Draw each option with ON/OFF status
|
|
272
|
-
|
|
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] ?
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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) %
|
|
323
|
+
@cursor = (@cursor - 1) % options_items.size
|
|
293
324
|
when :down
|
|
294
|
-
@cursor = (@cursor + 1) %
|
|
325
|
+
@cursor = (@cursor + 1) % options_items.size
|
|
295
326
|
when :enter
|
|
296
|
-
item =
|
|
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
|
|
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
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|