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.
- checksums.yaml +4 -4
- data/LICENSE +17 -0
- data/README.md +65 -4
- data/bin/doom +138 -25
- data/lib/doom/benchmark.rb +282 -0
- data/lib/doom/game/animations.rb +9 -2
- data/lib/doom/game/combat.rb +377 -105
- data/lib/doom/game/framebuffer_blitter.rb +38 -0
- data/lib/doom/game/geometry.rb +68 -0
- data/lib/doom/game/intermission.rb +231 -0
- data/lib/doom/game/item_pickup.rb +108 -60
- data/lib/doom/game/menu.rb +347 -0
- data/lib/doom/game/monster_ai.rb +304 -91
- data/lib/doom/game/player.rb +105 -0
- data/lib/doom/game/player_physics.rb +384 -0
- data/lib/doom/game/player_state.rb +47 -64
- data/lib/doom/game/random.rb +82 -0
- data/lib/doom/game/sector_actions.rb +466 -29
- data/lib/doom/game/sector_effects.rb +25 -18
- data/lib/doom/game/snapshot.rb +584 -0
- data/lib/doom/game/sound_engine.rb +176 -0
- 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 +820 -640
- data/lib/doom/platform/sdl.rb +74 -0
- data/lib/doom/platform/window_logic.rb +61 -0
- data/lib/doom/render/font.rb +80 -0
- 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 +684 -330
- data/lib/doom/render/renderer_factory.rb +50 -0
- data/lib/doom/render/screen_melt.rb +71 -0
- 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 +15 -17
- 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 +59 -10
- data/lib/doom/wad/sound.rb +93 -0
- data/lib/doom/wad/sprite.rb +60 -34
- data/lib/doom/wad/texture.rb +5 -5
- data/lib/doom/wad_downloader.rb +38 -56
- data/lib/doom.rb +235 -12
- 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
|