doom 0.2.0 → 0.3.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/README.md +75 -115
- data/bin/doom +47 -58
- data/lib/doom/map/data.rb +280 -0
- data/lib/doom/platform/gosu_window.rb +237 -0
- data/lib/doom/render/renderer.rb +1218 -0
- data/lib/doom/version.rb +1 -1
- data/lib/doom/wad/colormap.rb +38 -0
- data/lib/doom/wad/flat.rb +61 -0
- data/lib/doom/wad/palette.rb +37 -0
- data/lib/doom/wad/patch.rb +61 -0
- data/lib/doom/wad/reader.rb +79 -0
- data/lib/doom/wad/sprite.rb +205 -0
- data/lib/doom/wad/texture.rb +163 -0
- data/lib/doom/wad_downloader.rb +143 -0
- data/lib/doom.rb +56 -37
- metadata +32 -35
- data/LICENSE.txt +0 -21
- data/bin/console +0 -15
- data/bin/setup +0 -8
- data/bin/wad +0 -152
- data/lib/doom/bsp_renderer.rb +0 -90
- data/lib/doom/game.rb +0 -84
- data/lib/doom/hud.rb +0 -80
- data/lib/doom/map_loader.rb +0 -255
- data/lib/doom/renderer.rb +0 -32
- data/lib/doom/sprite_loader.rb +0 -88
- data/lib/doom/sprite_renderer.rb +0 -56
- data/lib/doom/texture_loader.rb +0 -138
- data/lib/doom/texture_mapper.rb +0 -57
- data/lib/doom/wad_loader.rb +0 -106
- data/lib/doom/window.rb +0 -41
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'gosu'
|
|
4
|
+
|
|
5
|
+
module Doom
|
|
6
|
+
module Platform
|
|
7
|
+
class GosuWindow < Gosu::Window
|
|
8
|
+
SCALE = 3
|
|
9
|
+
|
|
10
|
+
# Movement constants
|
|
11
|
+
MOVE_SPEED = 8.0 # Units per frame
|
|
12
|
+
TURN_SPEED = 3.0 # Degrees per frame
|
|
13
|
+
MOUSE_SENSITIVITY = 0.15 # Mouse look sensitivity
|
|
14
|
+
PLAYER_RADIUS = 16.0 # Collision radius
|
|
15
|
+
|
|
16
|
+
def initialize(renderer, palette, map)
|
|
17
|
+
super(Render::SCREEN_WIDTH * SCALE, Render::SCREEN_HEIGHT * SCALE, false)
|
|
18
|
+
self.caption = 'Doom Ruby'
|
|
19
|
+
|
|
20
|
+
@renderer = renderer
|
|
21
|
+
@palette = palette
|
|
22
|
+
@map = map
|
|
23
|
+
@screen_image = nil
|
|
24
|
+
@mouse_captured = false
|
|
25
|
+
@last_mouse_x = nil
|
|
26
|
+
|
|
27
|
+
# Pre-build palette lookup for speed
|
|
28
|
+
@palette_rgba = palette.colors.map { |r, g, b| [r, g, b, 255].pack('CCCC') }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def update
|
|
32
|
+
handle_input
|
|
33
|
+
@renderer.render_frame
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def handle_input
|
|
37
|
+
# Mouse look
|
|
38
|
+
handle_mouse_look
|
|
39
|
+
|
|
40
|
+
# Keyboard turning
|
|
41
|
+
if Gosu.button_down?(Gosu::KB_LEFT)
|
|
42
|
+
@renderer.turn(TURN_SPEED)
|
|
43
|
+
end
|
|
44
|
+
if Gosu.button_down?(Gosu::KB_RIGHT)
|
|
45
|
+
@renderer.turn(-TURN_SPEED)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Forward/backward movement
|
|
49
|
+
move_x = 0.0
|
|
50
|
+
move_y = 0.0
|
|
51
|
+
|
|
52
|
+
if Gosu.button_down?(Gosu::KB_UP) || Gosu.button_down?(Gosu::KB_W)
|
|
53
|
+
move_x += @renderer.cos_angle * MOVE_SPEED
|
|
54
|
+
move_y += @renderer.sin_angle * MOVE_SPEED
|
|
55
|
+
end
|
|
56
|
+
if Gosu.button_down?(Gosu::KB_DOWN) || Gosu.button_down?(Gosu::KB_S)
|
|
57
|
+
move_x -= @renderer.cos_angle * MOVE_SPEED
|
|
58
|
+
move_y -= @renderer.sin_angle * MOVE_SPEED
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Strafe
|
|
62
|
+
if Gosu.button_down?(Gosu::KB_A)
|
|
63
|
+
move_x += @renderer.sin_angle * MOVE_SPEED
|
|
64
|
+
move_y -= @renderer.cos_angle * MOVE_SPEED
|
|
65
|
+
end
|
|
66
|
+
if Gosu.button_down?(Gosu::KB_D)
|
|
67
|
+
move_x -= @renderer.sin_angle * MOVE_SPEED
|
|
68
|
+
move_y += @renderer.cos_angle * MOVE_SPEED
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Apply movement with collision detection
|
|
72
|
+
if move_x != 0.0 || move_y != 0.0
|
|
73
|
+
try_move(move_x, move_y)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def try_move(dx, dy)
|
|
78
|
+
new_x = @renderer.player_x + dx
|
|
79
|
+
new_y = @renderer.player_y + dy
|
|
80
|
+
|
|
81
|
+
# Check if new position is valid (simple collision detection)
|
|
82
|
+
if valid_position?(new_x, new_y)
|
|
83
|
+
@renderer.move_to(new_x, new_y)
|
|
84
|
+
update_player_height(new_x, new_y)
|
|
85
|
+
else
|
|
86
|
+
# Try sliding along walls - try X movement only
|
|
87
|
+
if valid_position?(new_x, @renderer.player_y)
|
|
88
|
+
@renderer.move_to(new_x, @renderer.player_y)
|
|
89
|
+
update_player_height(new_x, @renderer.player_y)
|
|
90
|
+
# Try Y movement only
|
|
91
|
+
elsif valid_position?(@renderer.player_x, new_y)
|
|
92
|
+
@renderer.move_to(@renderer.player_x, new_y)
|
|
93
|
+
update_player_height(@renderer.player_x, new_y)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def update_player_height(x, y)
|
|
99
|
+
sector = @map.sector_at(x, y)
|
|
100
|
+
return unless sector
|
|
101
|
+
|
|
102
|
+
# Player view height is 41 units above floor
|
|
103
|
+
target_z = sector.floor_height + 41
|
|
104
|
+
@renderer.set_z(target_z)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def valid_position?(x, y)
|
|
108
|
+
# Check if position is inside a valid sector
|
|
109
|
+
sector = @map.sector_at(x, y)
|
|
110
|
+
return false unless sector
|
|
111
|
+
|
|
112
|
+
# Check floor height - can't walk into walls
|
|
113
|
+
floor_height = sector.floor_height
|
|
114
|
+
return false if floor_height > @renderer.player_z + 24 # Max step height
|
|
115
|
+
|
|
116
|
+
# Check against blocking linedefs
|
|
117
|
+
@map.linedefs.each do |linedef|
|
|
118
|
+
next unless linedef_blocks?(linedef, x, y)
|
|
119
|
+
return false
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
true
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def linedef_blocks?(linedef, x, y)
|
|
126
|
+
v1 = @map.vertices[linedef.v1]
|
|
127
|
+
v2 = @map.vertices[linedef.v2]
|
|
128
|
+
|
|
129
|
+
# Check if player circle intersects this line
|
|
130
|
+
return false unless line_circle_intersect?(v1.x, v1.y, v2.x, v2.y, x, y, PLAYER_RADIUS)
|
|
131
|
+
|
|
132
|
+
# One-sided linedef (wall) always blocks
|
|
133
|
+
return true if linedef.sidedef_left == 0xFFFF
|
|
134
|
+
|
|
135
|
+
# Two-sided: check if passable
|
|
136
|
+
front_side = @map.sidedefs[linedef.sidedef_right]
|
|
137
|
+
back_side = @map.sidedefs[linedef.sidedef_left]
|
|
138
|
+
|
|
139
|
+
front_sector = @map.sectors[front_side.sector]
|
|
140
|
+
back_sector = @map.sectors[back_side.sector]
|
|
141
|
+
|
|
142
|
+
# Check step height
|
|
143
|
+
step = back_sector.floor_height - front_sector.floor_height
|
|
144
|
+
return true if step.abs > 24
|
|
145
|
+
|
|
146
|
+
# Check ceiling clearance
|
|
147
|
+
min_ceiling = [front_sector.ceiling_height, back_sector.ceiling_height].min
|
|
148
|
+
max_floor = [front_sector.floor_height, back_sector.floor_height].max
|
|
149
|
+
return true if min_ceiling - max_floor < 56 # Player height
|
|
150
|
+
|
|
151
|
+
false
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def line_circle_intersect?(x1, y1, x2, y2, cx, cy, radius)
|
|
155
|
+
# Vector from line start to circle center
|
|
156
|
+
dx = cx - x1
|
|
157
|
+
dy = cy - y1
|
|
158
|
+
|
|
159
|
+
# Line direction vector
|
|
160
|
+
line_dx = x2 - x1
|
|
161
|
+
line_dy = y2 - y1
|
|
162
|
+
line_len_sq = line_dx * line_dx + line_dy * line_dy
|
|
163
|
+
|
|
164
|
+
return false if line_len_sq == 0
|
|
165
|
+
|
|
166
|
+
# Project circle center onto line, clamped to segment
|
|
167
|
+
t = ((dx * line_dx) + (dy * line_dy)) / line_len_sq
|
|
168
|
+
t = [[t, 0.0].max, 1.0].min
|
|
169
|
+
|
|
170
|
+
# Closest point on line segment
|
|
171
|
+
closest_x = x1 + t * line_dx
|
|
172
|
+
closest_y = y1 + t * line_dy
|
|
173
|
+
|
|
174
|
+
# Distance from circle center to closest point
|
|
175
|
+
dist_x = cx - closest_x
|
|
176
|
+
dist_y = cy - closest_y
|
|
177
|
+
dist_sq = dist_x * dist_x + dist_y * dist_y
|
|
178
|
+
|
|
179
|
+
dist_sq < radius * radius
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def handle_mouse_look
|
|
183
|
+
return unless @mouse_captured
|
|
184
|
+
|
|
185
|
+
current_x = mouse_x
|
|
186
|
+
if @last_mouse_x
|
|
187
|
+
delta_x = current_x - @last_mouse_x
|
|
188
|
+
@renderer.turn(-delta_x * MOUSE_SENSITIVITY) if delta_x != 0
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Keep mouse centered
|
|
192
|
+
center_x = width / 2
|
|
193
|
+
if (current_x - center_x).abs > 50
|
|
194
|
+
self.mouse_x = center_x
|
|
195
|
+
@last_mouse_x = center_x
|
|
196
|
+
else
|
|
197
|
+
@last_mouse_x = current_x
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def draw
|
|
202
|
+
# Fast RGBA conversion using pre-built palette
|
|
203
|
+
rgba = @renderer.framebuffer.map { |idx| @palette_rgba[idx] }.join
|
|
204
|
+
|
|
205
|
+
@screen_image = Gosu::Image.from_blob(
|
|
206
|
+
Render::SCREEN_WIDTH,
|
|
207
|
+
Render::SCREEN_HEIGHT,
|
|
208
|
+
rgba
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
@screen_image.draw(0, 0, 0, SCALE, SCALE)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def button_down(id)
|
|
215
|
+
case id
|
|
216
|
+
when Gosu::KB_ESCAPE
|
|
217
|
+
if @mouse_captured
|
|
218
|
+
@mouse_captured = false
|
|
219
|
+
self.mouse_x = width / 2
|
|
220
|
+
self.mouse_y = height / 2
|
|
221
|
+
else
|
|
222
|
+
close
|
|
223
|
+
end
|
|
224
|
+
when Gosu::MS_LEFT, Gosu::KB_TAB
|
|
225
|
+
unless @mouse_captured
|
|
226
|
+
@mouse_captured = true
|
|
227
|
+
@last_mouse_x = mouse_x
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def needs_cursor?
|
|
233
|
+
!@mouse_captured
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|