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,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doom
4
+ module Platform
5
+ # SDL2 keyboard grab via Gosu's bundled SDL -- prevents OS key interception.
6
+ module SDLKeyboardGrab
7
+ def self.setup
8
+ require 'fiddle'
9
+ gosu_spec = Gem.loaded_specs['gosu']
10
+ lib_ext = RbConfig::CONFIG['DLEXT'] || 'so'
11
+ bundle = File.join(gosu_spec.full_gem_path, 'lib', "gosu.#{lib_ext}")
12
+ @lib = Fiddle.dlopen(bundle)
13
+ @shared_window = Fiddle::Function.new(
14
+ @lib['_ZN4Gosu13shared_windowEv'], [], Fiddle::TYPE_VOIDP
15
+ )
16
+ @set_kb_grab = Fiddle::Function.new(
17
+ @lib['SDL_SetWindowKeyboardGrab'],
18
+ [Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT], Fiddle::TYPE_VOID
19
+ )
20
+ @ready = true
21
+ rescue StandardError, LoadError => e
22
+ warn "SDLKeyboardGrab.setup failed: #{e.class}: #{e.message}"
23
+ @ready = false
24
+ end
25
+
26
+ def self.grab!
27
+ return unless @ready
28
+
29
+ @set_kb_grab.call(@shared_window.call, 1)
30
+ end
31
+
32
+ def self.release!
33
+ return unless @ready
34
+
35
+ @set_kb_grab.call(@shared_window.call, 0)
36
+ end
37
+ end
38
+
39
+ # Ask SDL for the display's refresh rate.
40
+ #
41
+ # Timing our own buffer swaps cannot answer this: when the engine renders
42
+ # slower than the display refreshes -- which is the normal case here --
43
+ # the swap cadence measures our render time, not the monitor. SDL knows
44
+ # the real number, and Gosu bundles SDL, so we read it directly the same
45
+ # way SDLKeyboardGrab reaches for SDL_SetWindowKeyboardGrab.
46
+ module SDLDisplayMode
47
+ # SDL_DisplayMode { Uint32 format; int w; int h; int refresh_rate; void *driverdata; }
48
+ REFRESH_RATE_OFFSET = 12
49
+ STRUCT_SIZE = 32
50
+
51
+ def self.refresh_rate
52
+ require 'fiddle'
53
+ gosu_spec = Gem.loaded_specs['gosu']
54
+ lib_ext = RbConfig::CONFIG['DLEXT'] || 'so'
55
+ lib = Fiddle.dlopen(File.join(gosu_spec.full_gem_path, 'lib', "gosu.#{lib_ext}"))
56
+
57
+ get_mode = Fiddle::Function.new(
58
+ lib['SDL_GetCurrentDisplayMode'],
59
+ [Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT
60
+ )
61
+
62
+ buf = Fiddle::Pointer.malloc(STRUCT_SIZE)
63
+ return nil unless get_mode.call(0, buf).zero?
64
+
65
+ hz = buf[REFRESH_RATE_OFFSET, 4].unpack1('l')
66
+ # SDL reports 0 when the rate is unspecified (some virtual displays).
67
+ hz.positive? ? hz : nil
68
+ rescue StandardError, LoadError => e
69
+ warn "SDLDisplayMode.refresh_rate failed: #{e.class}: #{e.message}"
70
+ nil
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doom
4
+ module Platform
5
+ # Pure helpers extracted from GosuWindow so they can be tested without a
6
+ # display. GosuWindow subclasses Gosu::Window, which cannot be loaded (let
7
+ # alone instantiated) under RSpec, so any decision logic worth testing lives
8
+ # here as plain functions and GosuWindow just delegates to them.
9
+ module WindowLogic
10
+ # DOOM thing flags: bit 0 = skill 1-2 (baby/easy), bit 1 = skill 3
11
+ # (medium), bit 2 = skill 4-5 (hard/nightmare), bit 4 = multiplayer-only.
12
+ SKILL_FLAG_EASY = 0x0001
13
+ SKILL_FLAG_MEDIUM = 0x0002
14
+ SKILL_FLAG_HARD = 0x0004
15
+ MULTIPLAYER_ONLY_FLAG = 0x0010
16
+
17
+ # The flag bit a thing must carry to appear at the given skill.
18
+ # skill uses Game::Menu::SKILL_* values (0-4).
19
+ def self.skill_flag_bit(skill)
20
+ case skill
21
+ when Game::Menu::SKILL_BABY, Game::Menu::SKILL_EASY then SKILL_FLAG_EASY
22
+ when Game::Menu::SKILL_MEDIUM then SKILL_FLAG_MEDIUM
23
+ when Game::Menu::SKILL_HARD, Game::Menu::SKILL_NIGHTMARE then SKILL_FLAG_HARD
24
+ else SKILL_FLAG_EASY | SKILL_FLAG_MEDIUM | SKILL_FLAG_HARD
25
+ end
26
+ end
27
+
28
+ # Which things this skill hides. A thing is hidden if it is
29
+ # multiplayer-only, or if it does not carry the flag bit for this skill.
30
+ # Returns { index => true } (matching the world's skill_hidden shape).
31
+ def self.skill_hidden(skill, things)
32
+ flag_bit = skill_flag_bit(skill)
33
+ hidden = {}
34
+ things.each_with_index do |thing, idx|
35
+ hidden[idx] = true if (thing.flags & MULTIPLAYER_ONLY_FLAG) != 0 || (thing.flags & flag_bit) == 0
36
+ end
37
+ hidden
38
+ end
39
+
40
+ # Frame-presentation cadence. Rendering runs every loop iteration, but we
41
+ # only blit/swap once a refresh interval (minus slack, so we aim slightly
42
+ # before vblank) has elapsed since the last present.
43
+ def self.present_due?(now_ms, last_present_ms, interval_ms, slack)
44
+ (now_ms - last_present_ms) >= (interval_ms * slack)
45
+ end
46
+
47
+ # Net status overlay lines. A lockstep stall under the threshold is normal
48
+ # -- every peer briefly waits for the next command between tics -- so it
49
+ # produces nothing; only a stall past the threshold, a session that has
50
+ # not started yet, or a desync is worth interrupting the player for.
51
+ def self.net_status_lines(started:, host:, waiting:, stalled_seconds:,
52
+ stall_threshold:, desync: nil)
53
+ lines = []
54
+ lines << (host ? 'WAITING FOR PLAYERS' : 'CONNECTING...') unless started
55
+ lines << "WAITING FOR PLAYER #{waiting.join(', ')}" if waiting.any? && stalled_seconds > stall_threshold
56
+ lines << "DESYNC AT TIC #{desync.tic} (#{Array(desync.sections).join(', ')})" if desync
57
+ lines
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doom
4
+ module Render
5
+ # DOOM's built-in font loaded from STCFN patches (ASCII 33-121).
6
+ # Uppercase only -- lowercase is auto-uppercased.
7
+ class Font
8
+ SPACE_WIDTH = 4
9
+
10
+ def initialize(_wad, hud_graphics)
11
+ @chars = {}
12
+ (33..121).each do |ascii|
13
+ name = 'STCFN%03d' % ascii
14
+ sprite = hud_graphics.send(:load_graphic, name)
15
+ @chars[ascii] = sprite if sprite
16
+ end
17
+ end
18
+
19
+ # Draw text into framebuffer at (x, y). Returns width drawn.
20
+ def draw_text(framebuffer, text, x, y, screen_width: 320, screen_height: 240)
21
+ cursor_x = x
22
+ text.upcase.each_char do |char|
23
+ if char == ' '
24
+ cursor_x += SPACE_WIDTH
25
+ next
26
+ end
27
+
28
+ sprite = @chars[char.ord]
29
+ next unless sprite
30
+
31
+ draw_char(framebuffer, sprite, cursor_x, y, screen_width, screen_height)
32
+ cursor_x += sprite.width
33
+ end
34
+ cursor_x - x
35
+ end
36
+
37
+ # Measure text width without drawing
38
+ def text_width(text)
39
+ width = 0
40
+ text.upcase.each_char do |char|
41
+ if char == ' '
42
+ width += SPACE_WIDTH
43
+ next
44
+ end
45
+ sprite = @chars[char.ord]
46
+ width += sprite.width if sprite
47
+ end
48
+ width
49
+ end
50
+
51
+ # Draw text centered horizontally
52
+ def draw_centered(framebuffer, text, y, screen_width: 320, screen_height: 240)
53
+ w = text_width(text)
54
+ x = (screen_width - w) / 2
55
+ draw_text(framebuffer, text, x, y, screen_width: screen_width, screen_height: screen_height)
56
+ end
57
+
58
+ private
59
+
60
+ def draw_char(framebuffer, sprite, x, y, screen_width, screen_height)
61
+ sprite.width.times do |col_x|
62
+ sx = x + col_x
63
+ next if sx < 0 || sx >= screen_width
64
+
65
+ col = sprite.column_pixels(col_x)
66
+ next unless col
67
+
68
+ col.each_with_index do |color, col_y|
69
+ next unless color
70
+
71
+ sy = y + col_y
72
+ next if sy < 0 || sy >= screen_height
73
+
74
+ framebuffer[(sy * screen_width) + sx] = color
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end