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.
@@ -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