doom 0.5.0 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c4672b03d393f17c1cd9ece97869d7239a7af3e91d3ffa47b911b0ba3058f673
4
- data.tar.gz: 56772f44ced59386c92f5319c0d25dc8612e96263d68ae7e9a240d26508571cf
3
+ metadata.gz: c3cb562e8b88b905638cfa8792337bc1b7f818753fa90587bda474784513a6bd
4
+ data.tar.gz: 1f421713b797c02dfc10e2f9bdd3ad0981a631f88bc94df161aa5a792b539094
5
5
  SHA512:
6
- metadata.gz: b11b52ed631217b002aae05a4f87b7ff5fcb208485b75dd9077ebcc222c3823c6ca74167e9bc6b7c3605c0481472abbc818e6445c8fa130c744fe8c54da53099
7
- data.tar.gz: 808176138162ef4e21796dd995a6bd794eee89e4c631032134349746afd468d446b610a0939daf19c26a2706abac22635306d6e3fb1c1c93e9a70282a2114f7d
6
+ metadata.gz: b06fd18ebfa1c5755e27c11b49e9e568fc4e2acfc7bc2b9760e7b7dda500a438afe9a6ecf0e33aa4f17c2271a6c28205e6bbb793e3b9c618c4df24b8bf74679a
7
+ data.tar.gz: ed5387b2c42963956cfe42c1cfe790a8f7f0e89a15c5a38934780b5ceaca34e3e64456756280d9c8f2d280763ef0fff89434dd8e826dc2e9e0184e25c4f94508
data/README.md CHANGED
@@ -1,17 +1,19 @@
1
1
  # DOOM Ruby
2
2
 
3
- A faithful ruby port of the DOOM (1993) rendering engine to Ruby.
3
+ A faithful port of the DOOM (1993) engine to pure Ruby. Renders the original WAD files with near pixel-perfect BSP rendering, full HUD, item pickups, and hitscan/projectile combat.
4
4
 
5
5
  ![DOOM Ruby](demo.gif)
6
6
 
7
7
  ## Features
8
8
 
9
- - Pure Ruby implementation of DOOM's BSP rendering engine
10
- - Accurate wall, floor, and ceiling rendering with proper texture mapping
11
- - Sprite rendering with depth-correct clipping
12
- - Original DOOM lighting and colormap support
13
- - Mouse look and WASD movement controls
14
- - Supports original WAD files (shareware and registered)
9
+ - **Rendering**: BSP traversal, visplanes, drawsegs, sprite clipping, sky rendering matching Chocolate Doom
10
+ - **Combat**: Hitscan weapons (pistol, shotgun, chaingun), melee (fist, chainsaw), projectile rockets with splash damage
11
+ - **Items**: Weapons, ammo, health, armor, keys -- all pickupable with correct DOOM behavior
12
+ - **Movement**: Momentum-based physics with friction, smooth step transitions, wall sliding, view bob
13
+ - **HUD**: Full status bar with ammo, health, armor, face, weapon selector, key cards, small ammo counts
14
+ - **Effects**: Animated textures (NUKAGE, SLADRIP), sector light effects (flickering, glowing, strobing), scrolling walls
15
+ - **Monsters**: Death animations, solid collision, HP tracking
16
+ - **Compatibility**: Supports original WAD files (shareware and registered), YJIT-optimized
15
17
 
16
18
  ## Installation
17
19
 
@@ -21,7 +23,7 @@ gem install doom
21
23
 
22
24
  ## Quick Start
23
25
 
24
- Just run `doom` - it will offer to download the free shareware version:
26
+ Just run `doom` -- it will offer to download the free shareware version:
25
27
 
26
28
  ```bash
27
29
  doom
@@ -44,11 +46,16 @@ doom /path/to/doom.wad
44
46
  | Left Arrow | Turn left |
45
47
  | Right Arrow | Turn right |
46
48
  | Mouse | Look around (click to capture) |
49
+ | Left Click / X / Shift | Fire weapon |
50
+ | Space / E | Use (open doors) |
51
+ | 1-7 | Switch weapons |
52
+ | M | Toggle automap |
53
+ | Z | Toggle debug overlay |
47
54
  | Escape | Release mouse / Quit |
48
55
 
49
56
  ## Requirements
50
57
 
51
- - Ruby 3.1 or higher
58
+ - Ruby 3.1+ (Ruby 4.0 with YJIT recommended for best performance)
52
59
  - Gosu gem (for window/graphics)
53
60
  - SDL2 (native library required by Gosu)
54
61
 
@@ -61,7 +68,7 @@ brew install sdl2
61
68
 
62
69
  **Ubuntu/Debian:**
63
70
  ```bash
64
- sudo apt-get install build-essential libsdl2-dev libgl1-mesa-dev libfontconfig1-dev
71
+ sudo apt-get install build-essential libsdl2-dev libgl1-mesa-dev libopenal-dev libsndfile1-dev libmpg123-dev libfontconfig1-dev
65
72
  ```
66
73
 
67
74
  **Fedora:**
@@ -75,15 +82,15 @@ sudo pacman -S sdl2 mesa
75
82
  ```
76
83
 
77
84
  **Windows:**
78
- No additional setup needed - the gem includes SDL2.
85
+ No additional setup needed -- the gem includes SDL2.
79
86
 
80
87
  ## Development
81
88
 
82
89
  ```bash
83
- git clone https://github.com/khasinski/doom-rb.git
84
- cd doom-rb
90
+ git clone https://github.com/khasinski/doom.git
91
+ cd doom
85
92
  bundle install
86
- ruby bin/doom doom1.wad
93
+ ruby bin/doom
87
94
  ```
88
95
 
89
96
  Run specs:
@@ -92,15 +99,30 @@ Run specs:
92
99
  bundle exec rspec
93
100
  ```
94
101
 
102
+ ### Benchmarking
103
+
104
+ ```bash
105
+ ruby bench/benchmark.rb # without YJIT
106
+ ruby --yjit bench/benchmark.rb # with YJIT
107
+ ruby bench/benchmark.rb --compare # side-by-side
108
+ ruby bench/benchmark.rb --profile # CPU profile with StackProf
109
+ ```
110
+
95
111
  ## Technical Details
96
112
 
97
- This implementation includes:
113
+ - **BSP Traversal**: Front-to-back rendering using the map's BSP tree with R_CheckBBox culling
114
+ - **Visplanes**: Floor/ceiling rendering with R_CheckPlane splitting and span-based drawing
115
+ - **Drawsegs**: Wall segment tracking for proper sprite clipping (silhouette system)
116
+ - **Texture Mapping**: Perspective-correct ray-seg intersection with non-power-of-2 support
117
+ - **Lighting**: Distance-based light diminishing with wall and flat colormaps
118
+ - **Sky Rendering**: Chocolate Doom sky hack (worldtop = worldhigh) with correct placement
119
+ - **Movement Physics**: Continuous-time momentum/friction matching Chocolate Doom's P_XYMovement
120
+ - **Hitscan**: Ray tracing against walls and monster bounding circles
121
+ - **Projectiles**: Physical rockets with wall/monster collision and splash damage
122
+
123
+ ## Performance
98
124
 
99
- - **BSP Traversal**: Front-to-back rendering using the map's BSP tree
100
- - **Visplanes**: Floor/ceiling rendering with R_CheckPlane splitting
101
- - **Drawsegs**: Wall segment tracking for proper sprite clipping
102
- - **Texture Mapping**: Perspective-correct texture coordinates
103
- - **Lighting**: Distance-based light diminishing with colormaps
125
+ With Ruby 4.0 and YJIT enabled, the renderer achieves 80-130 FPS on E1M1 (Apple Silicon). See [docs/performance-profiling.md](docs/performance-profiling.md) and [docs/yjit-vs-zjit.md](docs/yjit-vs-zjit.md) for detailed analysis.
104
126
 
105
127
  ## Legal
106
128
 
@@ -112,7 +134,7 @@ please purchase DOOM from [Steam](https://store.steampowered.com/app/2280/Ultima
112
134
 
113
135
  ## License
114
136
 
115
- GPL-2.0 - Same license as the original DOOM source code.
137
+ GPL-2.0 -- Same license as the original DOOM source code.
116
138
 
117
139
  ## Author
118
140
 
data/bin/doom CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- # Enable YJIT if available (Ruby 3.1+) for better performance
5
- if defined?(RubyVM::YJIT) && RubyVM::YJIT.respond_to?(:enable)
6
- RubyVM::YJIT.enable
4
+ # Enable YJIT unless --no-yjit flag is passed (for live toggle demo)
5
+ unless ARGV.delete('--no-yjit')
6
+ if defined?(RubyVM::YJIT) && RubyVM::YJIT.respond_to?(:enable)
7
+ RubyVM::YJIT.enable
8
+ end
7
9
  end
8
10
 
9
11
  # Parse arguments before loading heavy dependencies
@@ -50,6 +50,15 @@ module Doom
50
50
 
51
51
  DEATH_ANIM_TICS = 6 # Tics per death frame
52
52
 
53
+ # Projectile constants
54
+ ROCKET_SPEED = 20.0 # Map units per tic (matches DOOM's mobjinfo MISSILESPEED)
55
+ ROCKET_DAMAGE = 20 # Direct hit base (DOOM: 1d8 * 20)
56
+ ROCKET_RADIUS = 11 # Collision radius
57
+ SPLASH_RADIUS = 128.0 # Splash damage radius
58
+ SPLASH_DAMAGE = 128 # Max splash damage at center
59
+
60
+ Projectile = Struct.new(:x, :y, :z, :dx, :dy, :type, :spawn_tic)
61
+
53
62
  # Weapon damage: DOOM does (P_Random()%3 + 1) * multiplier
54
63
  # Pistol/chaingun: 1*5..3*5 = 5-15 per bullet
55
64
  # Shotgun: 7 pellets, each 1*5..3*5 = 5-15
@@ -61,10 +70,12 @@ module Doom
61
70
  @sprites = sprites
62
71
  @monster_hp = {} # thing_idx => current HP
63
72
  @dead_things = {} # thing_idx => { tic: death_start_tic, prefix: sprite_prefix }
73
+ @projectiles = [] # Active projectiles in flight
74
+ @explosions = [] # Active explosions (for rendering)
64
75
  @tic = 0
65
76
  end
66
77
 
67
- attr_reader :dead_things
78
+ attr_reader :dead_things, :projectiles, :explosions
68
79
 
69
80
  def dead?(thing_idx)
70
81
  @dead_things.key?(thing_idx)
@@ -88,6 +99,8 @@ module Doom
88
99
  # Called each game tic
89
100
  def update
90
101
  @tic += 1
102
+ update_projectiles
103
+ update_explosions
91
104
  end
92
105
 
93
106
  # Fire the current weapon
@@ -97,6 +110,8 @@ module Doom
97
110
  hitscan(px, py, cos_a, sin_a, 1, 0.0, 5)
98
111
  when PlayerState::WEAPON_SHOTGUN
99
112
  hitscan(px, py, cos_a, sin_a, 7, Math::PI / 32, 5)
113
+ when PlayerState::WEAPON_ROCKET
114
+ spawn_rocket(px, py, pz, cos_a, sin_a)
100
115
  when PlayerState::WEAPON_FIST
101
116
  melee(px, py, cos_a, sin_a, 2, 64)
102
117
  when PlayerState::WEAPON_CHAINSAW
@@ -106,6 +121,117 @@ module Doom
106
121
 
107
122
  private
108
123
 
124
+ def spawn_rocket(px, py, pz, cos_a, sin_a)
125
+ # Spawn slightly ahead of the player
126
+ @projectiles << Projectile.new(
127
+ px + cos_a * 20, py + sin_a * 20, pz,
128
+ cos_a * ROCKET_SPEED, sin_a * ROCKET_SPEED,
129
+ :rocket, @tic
130
+ )
131
+ end
132
+
133
+ def update_projectiles
134
+ @projectiles.reject! do |proj|
135
+ # Move projectile
136
+ new_x = proj.x + proj.dx
137
+ new_y = proj.y + proj.dy
138
+
139
+ # Check wall collision
140
+ hit_wall = hits_wall?(proj.x, proj.y, new_x, new_y)
141
+
142
+ # Check monster collision
143
+ hit_monster = nil
144
+ @map.things.each_with_index do |thing, idx|
145
+ next unless MONSTER_HP[thing.type]
146
+ next if @dead_things[idx]
147
+ radius = (MONSTER_RADIUS[thing.type] || 20) + ROCKET_RADIUS
148
+ dx = new_x - thing.x
149
+ dy = new_y - thing.y
150
+ if dx * dx + dy * dy < radius * radius
151
+ hit_monster = idx
152
+ break
153
+ end
154
+ end
155
+
156
+ if hit_wall || hit_monster
157
+ explode(new_x, new_y, hit_monster)
158
+ true # Remove projectile
159
+ else
160
+ proj.x = new_x
161
+ proj.y = new_y
162
+ false # Keep projectile
163
+ end
164
+ end
165
+ end
166
+
167
+ def explode(x, y, direct_hit_idx)
168
+ # Direct hit damage
169
+ if direct_hit_idx
170
+ damage = (rand(8) + 1) * ROCKET_DAMAGE
171
+ apply_damage(direct_hit_idx, damage)
172
+ end
173
+
174
+ # Splash damage to all monsters in radius
175
+ @map.things.each_with_index do |thing, idx|
176
+ next unless MONSTER_HP[thing.type]
177
+ next if @dead_things[idx]
178
+ next if idx == direct_hit_idx # Already took direct hit
179
+
180
+ dx = x - thing.x
181
+ dy = y - thing.y
182
+ dist = Math.sqrt(dx * dx + dy * dy)
183
+ next if dist >= SPLASH_RADIUS
184
+
185
+ # Damage falls off linearly with distance
186
+ damage = ((SPLASH_DAMAGE * (1.0 - dist / SPLASH_RADIUS))).to_i
187
+ apply_damage(idx, damage) if damage > 0
188
+ end
189
+
190
+ # Spawn explosion visual
191
+ @explosions << { x: x, y: y, tic: @tic }
192
+ end
193
+
194
+ def update_explosions
195
+ # Explosions last 20 tics then disappear
196
+ @explosions.reject! { |e| @tic - e[:tic] > 20 }
197
+ end
198
+
199
+ def hits_wall?(x1, y1, x2, y2)
200
+ @map.linedefs.each do |ld|
201
+ # One-sided walls always block
202
+ blocks = ld.sidedef_left == 0xFFFF || (ld.flags & 0x0001 != 0)
203
+ unless blocks
204
+ next unless ld.sidedef_left < 0xFFFF
205
+ front = @map.sidedefs[ld.sidedef_right]
206
+ back = @map.sidedefs[ld.sidedef_left]
207
+ fs = @map.sectors[front.sector]
208
+ bs = @map.sectors[back.sector]
209
+ max_floor = [fs.floor_height, bs.floor_height].max
210
+ min_ceil = [fs.ceiling_height, bs.ceiling_height].min
211
+ blocks = (min_ceil - max_floor) < 56
212
+ end
213
+ next unless blocks
214
+
215
+ v1 = @map.vertices[ld.v1]
216
+ v2 = @map.vertices[ld.v2]
217
+ if segments_intersect?(x1, y1, x2, y2, v1.x, v1.y, v2.x, v2.y)
218
+ return true
219
+ end
220
+ end
221
+ false
222
+ end
223
+
224
+ def segments_intersect?(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2)
225
+ d1x = ax2 - ax1; d1y = ay2 - ay1
226
+ d2x = bx2 - bx1; d2y = by2 - by1
227
+ denom = d1x * d2y - d1y * d2x
228
+ return false if denom.abs < 0.001
229
+ dx = bx1 - ax1; dy = by1 - ay1
230
+ t = (dx * d2y - dy * d2x).to_f / denom
231
+ u = (dx * d1y - dy * d1x).to_f / denom
232
+ t > 0.0 && t < 1.0 && u >= 0.0 && u <= 1.0
233
+ end
234
+
109
235
  def hitscan(px, py, cos_a, sin_a, pellets, spread, multiplier)
110
236
  pellets.times do
111
237
  # Add random spread
@@ -0,0 +1,295 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doom
4
+ module Game
5
+ # Basic monster AI: idle until seeing player, then chase.
6
+ # Matches Chocolate Doom's A_Look / A_Chase / P_NewChaseDir from p_enemy.c.
7
+ class MonsterAI
8
+ # 8 movement directions + no direction
9
+ DI_EAST = 0; DI_NORTHEAST = 1; DI_NORTH = 2; DI_NORTHWEST = 3
10
+ DI_WEST = 4; DI_SOUTHWEST = 5; DI_SOUTH = 6; DI_SOUTHEAST = 7
11
+ DI_NODIR = 8
12
+
13
+ # Movement deltas per direction (map units, 1.0 = FRACUNIT)
14
+ XSPEED = [1.0, 0.7071, 0.0, -0.7071, -1.0, -0.7071, 0.0, 0.7071].freeze
15
+ YSPEED = [0.0, 0.7071, 1.0, 0.7071, 0.0, -0.7071, -1.0, -0.7071].freeze
16
+
17
+ OPPOSITE = [DI_WEST, DI_SOUTHWEST, DI_SOUTH, DI_SOUTHEAST,
18
+ DI_EAST, DI_NORTHEAST, DI_NORTH, DI_NORTHWEST, DI_NODIR].freeze
19
+
20
+ # Monster speeds (from mobjinfo)
21
+ MONSTER_SPEED = {
22
+ 3004 => 8, 9 => 8, 3001 => 8, 3002 => 10, 58 => 10,
23
+ 3003 => 8, 69 => 8, 3005 => 8, 3006 => 8, 16 => 16,
24
+ 7 => 12, 65 => 8, 64 => 15, 71 => 8, 84 => 8,
25
+ }.freeze
26
+
27
+ CHASE_TICS = 4 # Steps between A_Chase calls
28
+ SIGHT_RANGE = 768.0 # Max distance for sight check (DOOM uses sector sound propagation, we approximate)
29
+ MELEE_RANGE = 64.0
30
+
31
+ # Direction to angle (for sprite facing)
32
+ DIR_ANGLES = [0, 45, 90, 135, 180, 225, 270, 315].freeze
33
+
34
+ MonsterState = Struct.new(:thing_idx, :x, :y, :movedir, :movecount,
35
+ :active, :chase_timer, :type)
36
+
37
+ def initialize(map, combat)
38
+ @map = map
39
+ @combat = combat
40
+ @monsters = []
41
+
42
+ map.things.each_with_index do |thing, idx|
43
+ next unless Combat::MONSTER_HP[thing.type]
44
+ @monsters << MonsterState.new(
45
+ idx, thing.x.to_f, thing.y.to_f,
46
+ DI_NODIR, 0, false, 0, thing.type
47
+ )
48
+ end
49
+ end
50
+
51
+ attr_reader :monsters
52
+
53
+ # Called each game tic
54
+ def update(player_x, player_y)
55
+ @monsters.each do |mon|
56
+ next if @combat.dead?(mon.thing_idx)
57
+
58
+ if mon.active
59
+ mon.chase_timer -= 1
60
+ if mon.chase_timer <= 0
61
+ mon.chase_timer = CHASE_TICS
62
+ chase(mon, player_x, player_y)
63
+ end
64
+ else
65
+ look(mon, player_x, player_y)
66
+ end
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def look(mon, player_x, player_y)
73
+ dx = player_x - mon.x
74
+ dy = player_y - mon.y
75
+ dist = Math.sqrt(dx * dx + dy * dy)
76
+ return if dist > SIGHT_RANGE
77
+
78
+ # DOOM A_Look: monster only sees in ~180-degree forward arc
79
+ # unless player is very close (melee range)
80
+ if dist > MELEE_RANGE
81
+ thing = @map.things[mon.thing_idx]
82
+ face_angle = thing.angle * Math::PI / 180.0
83
+ to_player = Math.atan2(dy, dx)
84
+ angle_diff = ((to_player - face_angle + Math::PI) % (2 * Math::PI) - Math::PI).abs
85
+ return if angle_diff > Math::PI / 2 # 90 degrees each side = 180 arc
86
+ end
87
+
88
+ if has_line_of_sight?(mon.x, mon.y, player_x, player_y)
89
+ mon.active = true
90
+ mon.chase_timer = CHASE_TICS
91
+ end
92
+ end
93
+
94
+ def chase(mon, player_x, player_y)
95
+ speed = MONSTER_SPEED[mon.type] || 8
96
+
97
+ # Decrement movecount; pick new direction when expired or blocked
98
+ mon.movecount -= 1
99
+ if mon.movecount < 0 || !try_move(mon, speed)
100
+ new_chase_dir(mon, player_x, player_y)
101
+ end
102
+
103
+ # Update the thing's position and facing angle in the map for rendering
104
+ thing = @map.things[mon.thing_idx]
105
+ thing.x = mon.x.to_i
106
+ thing.y = mon.y.to_i
107
+
108
+ # Face toward the player (smooth turning)
109
+ target_angle = Math.atan2(player_y - mon.y, player_x - mon.x) * 180.0 / Math::PI
110
+ thing.angle = target_angle.round.to_i
111
+ end
112
+
113
+ def try_move(mon, speed)
114
+ return false if mon.movedir == DI_NODIR
115
+
116
+ new_x = mon.x + speed * XSPEED[mon.movedir]
117
+ new_y = mon.y + speed * YSPEED[mon.movedir]
118
+
119
+ # Check if the position is valid (inside a sector, not blocked by walls)
120
+ sector = @map.sector_at(new_x, new_y)
121
+ return false unless sector
122
+
123
+ # Check wall collision
124
+ blocked = false
125
+ @map.linedefs.each do |ld|
126
+ v1 = @map.vertices[ld.v1]
127
+ v2 = @map.vertices[ld.v2]
128
+
129
+ # Simple line-circle intersection
130
+ radius = Combat::MONSTER_RADIUS[mon.type] || 20
131
+ next unless line_circle_intersect?(v1.x, v1.y, v2.x, v2.y, new_x, new_y, radius)
132
+
133
+ # One-sided walls always block
134
+ if ld.sidedef_left == 0xFFFF
135
+ blocked = true
136
+ break
137
+ end
138
+
139
+ # Two-sided: check step height and headroom
140
+ if ld.sidedef_left < 0xFFFF
141
+ front = @map.sectors[@map.sidedefs[ld.sidedef_right].sector]
142
+ back = @map.sectors[@map.sidedefs[ld.sidedef_left].sector]
143
+ step = (back.floor_height - front.floor_height).abs
144
+ min_ceil = [front.ceiling_height, back.ceiling_height].min
145
+ max_floor = [front.floor_height, back.floor_height].max
146
+ if step > 24 || (min_ceil - max_floor) < 56
147
+ blocked = true
148
+ break
149
+ end
150
+ end
151
+ end
152
+ return false if blocked
153
+
154
+ mon.x = new_x
155
+ mon.y = new_y
156
+ true
157
+ end
158
+
159
+ def new_chase_dir(mon, player_x, player_y)
160
+ deltax = player_x - mon.x
161
+ deltay = player_y - mon.y
162
+ old_dir = mon.movedir
163
+
164
+ # Determine preferred directions
165
+ dir_x = if deltax > 10 then DI_EAST
166
+ elsif deltax < -10 then DI_WEST
167
+ else DI_NODIR
168
+ end
169
+
170
+ dir_y = if deltay > 10 then DI_NORTH
171
+ elsif deltay < -10 then DI_SOUTH
172
+ else DI_NODIR
173
+ end
174
+
175
+ # Try diagonal
176
+ if dir_x != DI_NODIR && dir_y != DI_NODIR
177
+ diag = diagonal_dir(dir_x, dir_y)
178
+ if diag != OPPOSITE[old_dir]
179
+ mon.movedir = diag
180
+ if try_walk(mon)
181
+ return
182
+ end
183
+ end
184
+ end
185
+
186
+ # Randomly swap X/Y priority
187
+ if rand > 0.22 || deltay.abs > deltax.abs
188
+ dir_x, dir_y = dir_y, dir_x
189
+ end
190
+
191
+ # Try primary direction
192
+ if dir_x != DI_NODIR && dir_x != OPPOSITE[old_dir]
193
+ mon.movedir = dir_x
194
+ return if try_walk(mon)
195
+ end
196
+
197
+ # Try secondary direction
198
+ if dir_y != DI_NODIR && dir_y != OPPOSITE[old_dir]
199
+ mon.movedir = dir_y
200
+ return if try_walk(mon)
201
+ end
202
+
203
+ # Try old direction
204
+ if old_dir != DI_NODIR
205
+ mon.movedir = old_dir
206
+ return if try_walk(mon)
207
+ end
208
+
209
+ # Try all other directions
210
+ start = rand(8)
211
+ 8.times do |i|
212
+ d = (start + i) % 8
213
+ next if d == OPPOSITE[old_dir]
214
+ mon.movedir = d
215
+ return if try_walk(mon)
216
+ end
217
+
218
+ # Last resort: turnaround
219
+ if old_dir != DI_NODIR
220
+ mon.movedir = OPPOSITE[old_dir]
221
+ return if try_walk(mon)
222
+ end
223
+
224
+ mon.movedir = DI_NODIR
225
+ end
226
+
227
+ def try_walk(mon)
228
+ speed = MONSTER_SPEED[mon.type] || 8
229
+ if try_move(mon, speed)
230
+ mon.movecount = rand(16)
231
+ true
232
+ else
233
+ false
234
+ end
235
+ end
236
+
237
+ def diagonal_dir(dx, dy)
238
+ case [dx, dy]
239
+ when [DI_EAST, DI_NORTH] then DI_NORTHEAST
240
+ when [DI_EAST, DI_SOUTH] then DI_SOUTHEAST
241
+ when [DI_WEST, DI_NORTH] then DI_NORTHWEST
242
+ when [DI_WEST, DI_SOUTH] then DI_SOUTHWEST
243
+ else DI_NODIR
244
+ end
245
+ end
246
+
247
+ def has_line_of_sight?(x1, y1, x2, y2)
248
+ # Check if any wall blocks the line of sight
249
+ @map.linedefs.each do |ld|
250
+ v1 = @map.vertices[ld.v1]
251
+ v2 = @map.vertices[ld.v2]
252
+
253
+ next unless segments_intersect?(x1, y1, x2, y2, v1.x, v1.y, v2.x, v2.y)
254
+
255
+ # One-sided walls always block
256
+ return false if ld.sidedef_left == 0xFFFF
257
+
258
+ # Two-sided: check if opening is big enough to see through
259
+ if ld.sidedef_left < 0xFFFF
260
+ front = @map.sectors[@map.sidedefs[ld.sidedef_right].sector]
261
+ back = @map.sectors[@map.sidedefs[ld.sidedef_left].sector]
262
+ max_floor = [front.floor_height, back.floor_height].max
263
+ min_ceil = [front.ceiling_height, back.ceiling_height].min
264
+ # Block sight if the opening is too small
265
+ return false if (min_ceil - max_floor) < 1
266
+ end
267
+ end
268
+ true
269
+ end
270
+
271
+ def segments_intersect?(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2)
272
+ d1x = ax2 - ax1; d1y = ay2 - ay1
273
+ d2x = bx2 - bx1; d2y = by2 - by1
274
+ denom = d1x * d2y - d1y * d2x
275
+ return false if denom.abs < 0.001
276
+ dx = bx1 - ax1; dy = by1 - ay1
277
+ t = (dx * d2y - dy * d2x).to_f / denom
278
+ u = (dx * d1y - dy * d1x).to_f / denom
279
+ t > 0.0 && t < 1.0 && u >= 0.0 && u <= 1.0
280
+ end
281
+
282
+ def line_circle_intersect?(x1, y1, x2, y2, cx, cy, radius)
283
+ dx = cx - x1; dy = cy - y1
284
+ line_dx = x2 - x1; line_dy = y2 - y1
285
+ line_len_sq = line_dx * line_dx + line_dy * line_dy
286
+ return false if line_len_sq == 0
287
+ t = ((dx * line_dx) + (dy * line_dy)) / line_len_sq
288
+ t = [[t, 0.0].max, 1.0].min
289
+ closest_x = x1 + t * line_dx; closest_y = y1 + t * line_dy
290
+ dist_sq = (cx - closest_x) ** 2 + (cy - closest_y) ** 2
291
+ dist_sq < radius * radius
292
+ end
293
+ end
294
+ end
295
+ end
@@ -46,6 +46,8 @@ module Doom
46
46
  attr_accessor :attacking, :attack_frame, :attack_tics
47
47
  attr_accessor :bob_angle, :bob_amount
48
48
  attr_accessor :is_moving
49
+ attr_accessor :dead, :death_tic
50
+ attr_accessor :damage_count # Red flash intensity (0-8), decays each tic
49
51
 
50
52
  # Smooth step-up/down (matching Chocolate Doom's P_CalcHeight / P_ZMovement)
51
53
  VIEWHEIGHT = 41.0
@@ -107,6 +109,11 @@ module Doom
107
109
  @attack_frame = 0
108
110
  @attack_tics = 0
109
111
 
112
+ # Death state
113
+ @dead = false
114
+ @death_tic = 0
115
+ @damage_count = 0
116
+
110
117
  # Weapon bob
111
118
  @bob_angle = 0.0
112
119
  @bob_amount = 0.0
@@ -308,6 +315,42 @@ module Doom
308
315
 
309
316
  @weapon = weapon_num
310
317
  end
318
+
319
+ # Apply damage (from environment or enemies). Armor absorbs some.
320
+ def take_damage(amount)
321
+ return if @dead
322
+
323
+ absorbed = 0
324
+ if @armor > 0
325
+ absorbed = amount / 3 # Green armor absorbs 1/3
326
+ absorbed = @armor if absorbed > @armor
327
+ @armor -= absorbed
328
+ end
329
+
330
+ actual = amount - absorbed
331
+ @health -= actual
332
+
333
+ # Red flash proportional to damage (capped at palette 8)
334
+ @damage_count = [(@damage_count + actual / 2.0).ceil, 8].min
335
+
336
+ if @health <= 0
337
+ @health = 0
338
+ @damage_count = 8
339
+ die
340
+ end
341
+ end
342
+
343
+ # Decay damage flash each tic
344
+ def update_damage_count
345
+ @damage_count -= 1 if @damage_count > 0
346
+ end
347
+
348
+ def die
349
+ @dead = true
350
+ @death_tic = 0
351
+ @attacking = false
352
+ @deltaviewheight = -VIEWHEIGHT / 8.0 # View drops to ground
353
+ end
311
354
  end
312
355
  end
313
356
  end
@@ -48,9 +48,11 @@ module Doom
48
48
  2046 => 16, # Burning barrel
49
49
  }.freeze
50
50
 
51
- def initialize(renderer, palette, map, player_state = nil, status_bar = nil, weapon_renderer = nil, sector_actions = nil, animations = nil, sector_effects = nil, item_pickup = nil, combat = nil)
52
- super(Render::SCREEN_WIDTH * SCALE, Render::SCREEN_HEIGHT * SCALE, false)
51
+ def initialize(renderer, palette, map, player_state = nil, status_bar = nil, weapon_renderer = nil, sector_actions = nil, animations = nil, sector_effects = nil, item_pickup = nil, combat = nil, monster_ai = nil)
52
+ fullscreen = ARGV.include?('--fullscreen') || ARGV.include?('-f')
53
+ super(Render::SCREEN_WIDTH * SCALE, Render::SCREEN_HEIGHT * SCALE, fullscreen)
53
54
  self.caption = 'Doom Ruby'
55
+ self.update_interval = 0 # Uncap framerate (default 16.67ms = 60 FPS cap)
54
56
 
55
57
  @renderer = renderer
56
58
  @palette = palette
@@ -63,6 +65,7 @@ module Doom
63
65
  @sector_effects = sector_effects
64
66
  @item_pickup = item_pickup
65
67
  @combat = combat
68
+ @monster_ai = monster_ai
66
69
  @last_floor_height = nil
67
70
  @move_momx = 0.0
68
71
  @move_momy = 0.0
@@ -76,12 +79,21 @@ module Doom
76
79
  @show_debug = false
77
80
  @show_map = false
78
81
  @debug_font = Gosu::Font.new(16)
82
+ @fps_frames = 0
83
+ @fps_time = Time.now
84
+ @fps_display = 0.0
79
85
 
80
86
  # Precompute sector colors for automap
81
87
  @sector_colors = build_sector_colors
82
88
 
83
- # Pre-build palette lookup for speed
84
- @palette_rgba = palette.colors.map { |r, g, b| [r, g, b, 255].pack('CCCC') }
89
+ # Pre-build palette RGBA lookups for all 14 palettes (0=normal, 1-8=pain red)
90
+ @all_palette_rgba = []
91
+ wad = renderer.instance_variable_get(:@wad)
92
+ 14.times do |pal_idx|
93
+ pal = Wad::Palette.load(wad, pal_idx)
94
+ @all_palette_rgba << pal.colors.map { |r, g, b| [r, g, b, 255].pack('CCCC') }
95
+ end
96
+ @palette_rgba = @all_palette_rgba[0]
85
97
  end
86
98
 
87
99
  def update
@@ -107,6 +119,19 @@ module Doom
107
119
  @player_state&.update_viewheight
108
120
  @player_state&.update_attack # Attack timing at 35fps like DOOM
109
121
  @combat&.update
122
+ @monster_ai&.update(@renderer.player_x, @renderer.player_y)
123
+
124
+ @player_state&.update_damage_count
125
+
126
+ # Sector damage (nukage, lava, etc.) every 32 tics
127
+ if @player_state && !@player_state.dead && (@leveltime % 32 == 0)
128
+ check_sector_damage
129
+ end
130
+
131
+ # Track death tic for death animation
132
+ if @player_state&.dead
133
+ @player_state.death_tic += 1
134
+ end
110
135
  end
111
136
  @animations&.update(@leveltime)
112
137
 
@@ -127,20 +152,38 @@ module Doom
127
152
 
128
153
  # Pass combat state to renderer for death frame rendering
129
154
  @renderer.combat = @combat
155
+ @renderer.monster_ai = @monster_ai
156
+ @renderer.leveltime = @leveltime
130
157
 
131
158
  # Render the 3D world
132
159
  @renderer.render_frame
133
160
 
134
161
  # Render HUD on top
135
- if @weapon_renderer
162
+ if @weapon_renderer && !@player_state&.dead
136
163
  @weapon_renderer.render(@renderer.framebuffer)
137
164
  end
138
165
  if @status_bar
139
166
  @status_bar.render(@renderer.framebuffer)
140
167
  end
168
+
169
+ # Red tint when dead
170
+ if @player_state&.dead
171
+ apply_death_tint(@renderer.framebuffer)
172
+ end
141
173
  end
142
174
 
143
175
  def handle_input(delta_time)
176
+ # Handle respawn when dead
177
+ if @player_state&.dead
178
+ if @player_state.death_tic > 35 # 1 second delay before respawn allowed
179
+ if Gosu.button_down?(Gosu::KB_SPACE) || Gosu.button_down?(Gosu::KB_X) ||
180
+ Gosu.button_down?(Gosu::MS_LEFT) || Gosu.button_down?(Gosu::KB_LEFT_SHIFT)
181
+ respawn_player
182
+ end
183
+ end
184
+ return # No other input while dead
185
+ end
186
+
144
187
  # Mouse look
145
188
  handle_mouse_look
146
189
 
@@ -616,8 +659,10 @@ module Doom
616
659
  if @show_map
617
660
  draw_automap
618
661
  else
619
- # Fast RGBA conversion using pre-built palette
620
- rgba = @renderer.framebuffer.map { |idx| @palette_rgba[idx] }.join
662
+ # Select palette: red tint when taking damage (palettes 1-8)
663
+ pal_idx = @player_state ? @player_state.damage_count.clamp(0, 8) : 0
664
+ active_pal = @all_palette_rgba[pal_idx]
665
+ rgba = @renderer.framebuffer.map { |idx| active_pal[idx] }.join
621
666
 
622
667
  @screen_image = Gosu::Image.from_blob(
623
668
  Render::SCREEN_WIDTH,
@@ -632,6 +677,16 @@ module Doom
632
677
  end
633
678
 
634
679
  def draw_debug_overlay
680
+ # Update FPS counter (refresh every 0.5 seconds)
681
+ @fps_frames += 1
682
+ now = Time.now
683
+ elapsed = now - @fps_time
684
+ if elapsed >= 0.5
685
+ @fps_display = (@fps_frames / elapsed).round(1)
686
+ @fps_frames = 0
687
+ @fps_time = now
688
+ end
689
+
635
690
  sector = @map.sector_at(@renderer.player_x, @renderer.player_y)
636
691
  return unless sector
637
692
 
@@ -639,12 +694,14 @@ module Doom
639
694
  sector_idx = @map.sectors.index(sector)
640
695
 
641
696
  lines = [
697
+ "FPS: #{@fps_display}",
642
698
  "Sector #{sector_idx}",
643
699
  "Floor: #{sector.floor_height} (#{sector.floor_texture})",
644
700
  "Ceil: #{sector.ceiling_height} (#{sector.ceiling_texture})",
645
701
  "Light: #{sector.light_level}",
646
702
  "Pos: #{@renderer.player_x.round}, #{@renderer.player_y.round}",
647
703
  "Heading: #{(Math.atan2(@renderer.sin_angle, @renderer.cos_angle) * 180.0 / Math::PI).round(1)}",
704
+ "YJIT: #{defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled? ? 'ON' : 'OFF'} (Y to toggle)",
648
705
  ]
649
706
 
650
707
  y = 4
@@ -672,6 +729,17 @@ module Doom
672
729
  end
673
730
  when Gosu::KB_Z
674
731
  @show_debug = !@show_debug
732
+ when Gosu::KB_Y
733
+ if defined?(RubyVM::YJIT)
734
+ setup_yjit_toggle
735
+ if RubyVM::YJIT.enabled?
736
+ RubyVM::YJIT.disable
737
+ puts "YJIT disabled!"
738
+ else
739
+ RubyVM::YJIT.enable
740
+ puts "YJIT enabled!"
741
+ end
742
+ end
675
743
  when Gosu::KB_M
676
744
  @show_map = !@show_map
677
745
  when Gosu::KB_F12
@@ -679,6 +747,70 @@ module Doom
679
747
  end
680
748
  end
681
749
 
750
+ # Sector damage types from DOOM (p_spec.c)
751
+ # Type 5: 10 damage, Type 7: 5 damage, Type 4/16: 20 damage
752
+ SECTOR_DAMAGE = { 5 => 10, 7 => 5, 4 => 20, 16 => 20, 11 => 20 }.freeze
753
+
754
+ def check_sector_damage
755
+ sector = @map.sector_at(@renderer.player_x, @renderer.player_y)
756
+ return unless sector
757
+
758
+ damage = SECTOR_DAMAGE[sector.special]
759
+ @player_state.take_damage(damage) if damage
760
+ end
761
+
762
+ def apply_death_tint(framebuffer)
763
+ # Death keeps damage_count at max so the pain palette stays red
764
+ @player_state.damage_count = 8 if @player_state&.dead
765
+ end
766
+
767
+ def respawn_player
768
+ @player_state.reset
769
+ @last_floor_height = nil
770
+ @move_momx = 0.0
771
+ @move_momy = 0.0
772
+
773
+ # Reset item pickup, combat, and monster AI state
774
+ sprites = @combat&.instance_variable_get(:@sprites)
775
+ @item_pickup = Game::ItemPickup.new(@map, @player_state) if @item_pickup
776
+ @combat = Game::Combat.new(@map, @player_state, sprites) if @combat && sprites
777
+ @monster_ai = Game::MonsterAI.new(@map, @combat) if @monster_ai && @combat
778
+
779
+ # Move player to start position
780
+ ps = @map.player_start
781
+ if ps
782
+ @renderer.set_player(ps.x, ps.y, 41, ps.angle)
783
+ update_player_height(ps.x, ps.y)
784
+ end
785
+ end
786
+
787
+ def setup_yjit_toggle
788
+ return if @yjit_toggle_ready || !defined?(RubyVM::YJIT)
789
+ require "fiddle"
790
+
791
+ address = Fiddle::Handle::DEFAULT["rb_yjit_enabled_p"]
792
+ enabled_ptr = Fiddle::Pointer.new(address, Fiddle::SIZEOF_CHAR)
793
+
794
+ RubyVM::YJIT.singleton_class.prepend(Module.new do
795
+ define_method(:enable) do |**kwargs|
796
+ return false if enabled?
797
+ return super(**kwargs) unless RUBY_DESCRIPTION.include?("+YJIT")
798
+ enabled_ptr[0] = 1
799
+ true
800
+ end
801
+
802
+ define_method(:disable) do
803
+ return false unless enabled?
804
+ enabled_ptr[0] = 0
805
+ true
806
+ end
807
+ end)
808
+
809
+ @yjit_toggle_ready = true
810
+ rescue => e
811
+ puts "YJIT toggle setup failed: #{e.message}"
812
+ end
813
+
682
814
  def needs_cursor?
683
815
  !@mouse_captured
684
816
  end
@@ -64,6 +64,8 @@ module Doom
64
64
  @animations = animations
65
65
  @hidden_things = nil
66
66
  @combat = nil
67
+ @monster_ai = nil
68
+ @leveltime = 0
67
69
 
68
70
  @framebuffer = Array.new(SCREEN_WIDTH * SCREEN_HEIGHT, 0)
69
71
 
@@ -104,7 +106,7 @@ module Doom
104
106
  end
105
107
 
106
108
  attr_reader :player_x, :player_y, :player_z, :sin_angle, :cos_angle, :framebuffer
107
- attr_writer :hidden_things, :combat
109
+ attr_writer :hidden_things, :combat, :monster_ai, :leveltime
108
110
 
109
111
  # Diagnostic: returns info about all sprites and why they are/aren't visible
110
112
  def sprite_diagnostics
@@ -1302,9 +1304,17 @@ module Doom
1302
1304
  dy = thing.y - @player_y
1303
1305
  angle_to_thing = Math.atan2(dy, dx)
1304
1306
 
1305
- # Get sprite - use death frame if monster is dead
1307
+ # Get sprite: death frame > walking frame > idle frame
1306
1308
  if @combat && @combat.dead?(thing_idx)
1307
1309
  sprite = @combat.death_sprite(thing_idx, thing.type, angle_to_thing, thing.angle)
1310
+ elsif @monster_ai
1311
+ mon = @monster_ai.monsters.find { |m| m.thing_idx == thing_idx }
1312
+ if mon && mon.active
1313
+ # Walking animation: cycle through frames A-D based on leveltime
1314
+ walk_frame = %w[A B C D][@leveltime / 4 % 4]
1315
+ sprite = @sprites.get_frame(thing.type, walk_frame, angle_to_thing, thing.angle)
1316
+ end
1317
+ sprite ||= @sprites.get_rotated(thing.type, angle_to_thing, thing.angle)
1308
1318
  else
1309
1319
  sprite = @sprites.get_rotated(thing.type, angle_to_thing, thing.angle)
1310
1320
  end
@@ -1328,6 +1338,45 @@ module Doom
1328
1338
  visible_sprites.each do |vs|
1329
1339
  draw_sprite(vs)
1330
1340
  end
1341
+
1342
+ # Draw projectiles and explosions
1343
+ render_projectiles if @combat
1344
+ end
1345
+
1346
+ def render_projectiles
1347
+ # Render rockets in flight
1348
+ @combat.projectiles.each do |proj|
1349
+ view_x, view_y = transform_point(proj.x, proj.y)
1350
+ next if view_y <= 0
1351
+
1352
+ # Rocket has rotations 1-8 based on travel direction relative to viewer
1353
+ rocket_angle = Math.atan2(proj.dy, proj.dx)
1354
+ viewer_angle = Math.atan2(proj.y - @player_y, proj.x - @player_x)
1355
+ angle_diff = (viewer_angle - rocket_angle) % (2 * Math::PI)
1356
+ rotation = ((angle_diff + Math::PI / 8) / (Math::PI / 4)).to_i % 8 + 1
1357
+ rocket_sprite = @sprites.send(:load_sprite_frame, 'MISL', 'A', rotation)
1358
+ next unless rocket_sprite
1359
+
1360
+ screen_x = HALF_WIDTH + (view_x * @projection / view_y)
1361
+ thing_stub = Map::Thing.new(proj.x, proj.y, 0, 0, 0)
1362
+ visible = VisibleSprite.new(thing_stub, rocket_sprite, view_x, view_y, view_y, screen_x)
1363
+ draw_sprite(visible)
1364
+ end
1365
+
1366
+ # Render explosions (frames B, C, D - all rotation 0)
1367
+ @combat.explosions.each do |expl|
1368
+ view_x, view_y = transform_point(expl[:x], expl[:y])
1369
+ next if view_y <= 0
1370
+ elapsed = (@combat.instance_variable_get(:@tic) - expl[:tic])
1371
+ frame_idx = (elapsed / 4).clamp(0, 2)
1372
+ frame_letter = %w[B C D][frame_idx]
1373
+ expl_sprite = @sprites.send(:load_sprite_frame, 'MISL', frame_letter, 0)
1374
+ next unless expl_sprite
1375
+ screen_x = HALF_WIDTH + (view_x * @projection / view_y)
1376
+ thing_stub = Map::Thing.new(expl[:x], expl[:y], 0, 0, 0)
1377
+ visible = VisibleSprite.new(thing_stub, expl_sprite, view_x, view_y, view_y, screen_x)
1378
+ draw_sprite(visible)
1379
+ end
1331
1380
  end
1332
1381
 
1333
1382
  def draw_sprite(vs)
data/lib/doom/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Doom
4
- VERSION = '0.5.0'
4
+ VERSION = '0.6.0'
5
5
  end
@@ -178,8 +178,9 @@ module Doom
178
178
  return sprite if sprite
179
179
 
180
180
  # Calculate rotation frame (1-8)
181
- # Doom rotations: 1=front, 2=front-right, 3=right, etc. (clockwise)
182
- angle_diff = viewer_angle - (thing_angle * Math::PI / 180.0)
181
+ # DOOM: rot = (R_PointToAngle(thing) - thing->angle + ANG45/2*9) >> 29
182
+ # Rotation 1=front (viewer faces monster's front), 5=back
183
+ angle_diff = viewer_angle - (thing_angle * Math::PI / 180.0) + Math::PI
183
184
  angle_diff = angle_diff % (2 * Math::PI)
184
185
  angle_diff += 2 * Math::PI if angle_diff < 0
185
186
  rotation = ((angle_diff + Math::PI / 8) / (Math::PI / 4)).to_i % 8 + 1
@@ -196,8 +197,8 @@ module Doom
196
197
  sprite = load_sprite_frame(prefix, frame_letter, 0)
197
198
  return sprite if sprite
198
199
 
199
- # Try with calculated rotation
200
- angle_diff = viewer_angle - (thing_angle * Math::PI / 180.0)
200
+ # Try with calculated rotation (same formula as get_rotated)
201
+ angle_diff = viewer_angle - (thing_angle * Math::PI / 180.0) + Math::PI
201
202
  angle_diff = angle_diff % (2 * Math::PI)
202
203
  angle_diff += 2 * Math::PI if angle_diff < 0
203
204
  rotation = ((angle_diff + Math::PI / 8) / (Math::PI / 4)).to_i % 8 + 1
@@ -66,7 +66,16 @@ module Doom
66
66
  uri = URI.parse(SHAREWARE_URL)
67
67
  downloaded = 0
68
68
 
69
- Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
69
+ ssl_opts = { use_ssl: uri.scheme == 'https' }
70
+ # Workaround for SSL certificate issues on some systems
71
+ begin
72
+ Net::HTTP.start(uri.host, uri.port, **ssl_opts) { |h| h.head(uri) }
73
+ rescue OpenSSL::SSL::SSLError
74
+ puts "SSL certificate verification failed, retrying without verification..."
75
+ ssl_opts[:verify_mode] = OpenSSL::SSL::VERIFY_NONE
76
+ end
77
+
78
+ Net::HTTP.start(uri.host, uri.port, **ssl_opts) do |http|
70
79
  request = Net::HTTP::Get.new(uri)
71
80
 
72
81
  http.request(request) do |response|
data/lib/doom.rb CHANGED
@@ -17,6 +17,7 @@ require_relative 'doom/game/animations'
17
17
  require_relative 'doom/game/item_pickup'
18
18
  require_relative 'doom/game/combat'
19
19
  require_relative 'doom/game/sector_effects'
20
+ require_relative 'doom/game/monster_ai'
20
21
  require_relative 'doom/render/renderer'
21
22
  require_relative 'doom/render/status_bar'
22
23
  require_relative 'doom/render/weapon_renderer'
@@ -81,9 +82,10 @@ module Doom
81
82
  sector_effects = Game::SectorEffects.new(map)
82
83
  item_pickup = Game::ItemPickup.new(map, player_state)
83
84
  combat = Game::Combat.new(map, player_state, sprites)
85
+ monster_ai = Game::MonsterAI.new(map, combat)
84
86
 
85
87
  puts 'Starting game window...'
86
- window = Platform::GosuWindow.new(renderer, palette, map, player_state, status_bar, weapon_renderer, sector_actions, animations, sector_effects, item_pickup, combat)
88
+ window = Platform::GosuWindow.new(renderer, palette, map, player_state, status_bar, weapon_renderer, sector_actions, animations, sector_effects, item_pickup, combat, monster_ai)
87
89
  window.show
88
90
  end
89
91
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: doom
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Hasinski
@@ -66,6 +66,7 @@ files:
66
66
  - lib/doom/game/animations.rb
67
67
  - lib/doom/game/combat.rb
68
68
  - lib/doom/game/item_pickup.rb
69
+ - lib/doom/game/monster_ai.rb
69
70
  - lib/doom/game/player_state.rb
70
71
  - lib/doom/game/sector_actions.rb
71
72
  - lib/doom/game/sector_effects.rb
@@ -84,7 +85,7 @@ files:
84
85
  - lib/doom/wad/sprite.rb
85
86
  - lib/doom/wad/texture.rb
86
87
  - lib/doom/wad_downloader.rb
87
- homepage: https://github.com/khasinski/doom-rb
88
+ homepage: https://github.com/khasinski/doom
88
89
  licenses:
89
90
  - GPL-2.0
90
91
  metadata: {}
@@ -102,7 +103,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
102
103
  - !ruby/object:Gem::Version
103
104
  version: '0'
104
105
  requirements: []
105
- rubygems_version: 4.0.3
106
+ rubygems_version: 4.0.6
106
107
  specification_version: 4
107
108
  summary: Doom engine port in pure Ruby
108
109
  test_files: []