doom 0.3.0 → 0.5.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 +1 -1
- data/lib/doom/game/animations.rb +97 -0
- data/lib/doom/game/combat.rb +244 -0
- data/lib/doom/game/item_pickup.rb +170 -0
- data/lib/doom/game/player_state.rb +313 -0
- data/lib/doom/game/sector_actions.rb +162 -0
- data/lib/doom/game/sector_effects.rb +179 -0
- data/lib/doom/platform/gosu_window.rb +706 -59
- data/lib/doom/render/renderer.rb +397 -136
- data/lib/doom/render/status_bar.rb +218 -0
- data/lib/doom/render/weapon_renderer.rb +99 -0
- data/lib/doom/version.rb +1 -1
- data/lib/doom/wad/colormap.rb +0 -6
- data/lib/doom/wad/flat.rb +0 -21
- data/lib/doom/wad/hud_graphics.rb +257 -0
- data/lib/doom/wad/sprite.rb +95 -22
- data/lib/doom/wad/texture.rb +23 -22
- data/lib/doom/wad_downloader.rb +2 -2
- data/lib/doom.rb +27 -2
- metadata +12 -6
|
@@ -7,33 +7,140 @@ module Doom
|
|
|
7
7
|
class GosuWindow < Gosu::Window
|
|
8
8
|
SCALE = 3
|
|
9
9
|
|
|
10
|
-
# Movement constants
|
|
11
|
-
|
|
10
|
+
# Movement constants (matching Chocolate Doom P_Thrust / P_XYMovement)
|
|
11
|
+
# DOOM: terminal walk speed = 7.55 units/tic = 264 units/sec
|
|
12
|
+
# Continuous-time: v_terminal = thrust_rate / decay_rate
|
|
13
|
+
# decay_rate = -ln(0.90625) * 35 = 3.44/sec
|
|
14
|
+
# thrust_rate = 264 * 3.44 = 908 units/sec^2
|
|
15
|
+
MOVE_THRUST_RATE = 264.0 * 3.44 # Thrust rate (units/sec^2)
|
|
16
|
+
FRICTION_DECAY_RATE = 3.44 # Friction decay (1/sec)
|
|
17
|
+
STOPSPEED = 0.5 # Snap-to-zero threshold (units/sec)
|
|
12
18
|
TURN_SPEED = 3.0 # Degrees per frame
|
|
13
19
|
MOUSE_SENSITIVITY = 0.15 # Mouse look sensitivity
|
|
14
20
|
PLAYER_RADIUS = 16.0 # Collision radius
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
USE_DISTANCE = 64.0 # Max distance to use a linedef
|
|
23
|
+
|
|
24
|
+
# Solid thing types with their collision radii (from mobjinfo[] MF_SOLID)
|
|
25
|
+
# Monsters, barrels, pillars, lamps, torches, trees block player movement
|
|
26
|
+
SOLID_THING_RADIUS = {
|
|
27
|
+
9 => 20, 65 => 20, 66 => 20, 67 => 20, 68 => 20, # Shotgun Guy variants
|
|
28
|
+
3004 => 20, 84 => 20, # Zombieman
|
|
29
|
+
3001 => 20, # Imp
|
|
30
|
+
3002 => 30, 58 => 30, # Demon, Spectre
|
|
31
|
+
3003 => 24, 69 => 24, # Baron, Hell Knight
|
|
32
|
+
3006 => 16, # Lost Soul
|
|
33
|
+
3005 => 31, # Cacodemon
|
|
34
|
+
16 => 40, # Cyberdemon
|
|
35
|
+
7 => 128, # Spider Mastermind
|
|
36
|
+
64 => 20, # Archvile
|
|
37
|
+
71 => 31, # Pain Elemental
|
|
38
|
+
2035 => 10, # Barrel
|
|
39
|
+
2028 => 16, # Tall lamp
|
|
40
|
+
48 => 16, 30 => 16, 32 => 16, # Tech column, green/red pillars
|
|
41
|
+
31 => 16, 33 => 16, 36 => 16, # Short pillars
|
|
42
|
+
41 => 16, 43 => 16, # Evil eye, burnt tree
|
|
43
|
+
54 => 32, # Brown tree
|
|
44
|
+
44 => 16, 45 => 16, 46 => 16, # Tall torches
|
|
45
|
+
55 => 16, 56 => 16, 57 => 16, # Short torches
|
|
46
|
+
47 => 16, 70 => 16, # Stubs
|
|
47
|
+
85 => 16, 86 => 16, # Tall tech lamps
|
|
48
|
+
2046 => 16, # Burning barrel
|
|
49
|
+
}.freeze
|
|
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)
|
|
17
52
|
super(Render::SCREEN_WIDTH * SCALE, Render::SCREEN_HEIGHT * SCALE, false)
|
|
18
53
|
self.caption = 'Doom Ruby'
|
|
19
54
|
|
|
20
55
|
@renderer = renderer
|
|
21
56
|
@palette = palette
|
|
22
57
|
@map = map
|
|
58
|
+
@player_state = player_state
|
|
59
|
+
@status_bar = status_bar
|
|
60
|
+
@weapon_renderer = weapon_renderer
|
|
61
|
+
@sector_actions = sector_actions
|
|
62
|
+
@animations = animations
|
|
63
|
+
@sector_effects = sector_effects
|
|
64
|
+
@item_pickup = item_pickup
|
|
65
|
+
@combat = combat
|
|
66
|
+
@last_floor_height = nil
|
|
67
|
+
@move_momx = 0.0
|
|
68
|
+
@move_momy = 0.0
|
|
69
|
+
@leveltime = 0
|
|
70
|
+
@tic_accumulator = 0.0
|
|
23
71
|
@screen_image = nil
|
|
24
72
|
@mouse_captured = false
|
|
25
73
|
@last_mouse_x = nil
|
|
74
|
+
@last_update_time = Time.now
|
|
75
|
+
@use_pressed = false
|
|
76
|
+
@show_debug = false
|
|
77
|
+
@show_map = false
|
|
78
|
+
@debug_font = Gosu::Font.new(16)
|
|
79
|
+
|
|
80
|
+
# Precompute sector colors for automap
|
|
81
|
+
@sector_colors = build_sector_colors
|
|
26
82
|
|
|
27
83
|
# Pre-build palette lookup for speed
|
|
28
84
|
@palette_rgba = palette.colors.map { |r, g, b| [r, g, b, 255].pack('CCCC') }
|
|
29
85
|
end
|
|
30
86
|
|
|
31
87
|
def update
|
|
32
|
-
|
|
88
|
+
# Calculate delta time for smooth animations
|
|
89
|
+
now = Time.now
|
|
90
|
+
delta_time = now - @last_update_time
|
|
91
|
+
@last_update_time = now
|
|
92
|
+
|
|
93
|
+
handle_input(delta_time)
|
|
94
|
+
|
|
95
|
+
# Update player state (per-frame for smooth bob)
|
|
96
|
+
if @player_state
|
|
97
|
+
@player_state.update_bob(delta_time)
|
|
98
|
+
@player_state.update_view_bob(delta_time)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Advance game tics at 35/sec (DOOM's tic rate)
|
|
102
|
+
@tic_accumulator += delta_time * 35.0
|
|
103
|
+
while @tic_accumulator >= 1.0
|
|
104
|
+
@leveltime += 1
|
|
105
|
+
@tic_accumulator -= 1.0
|
|
106
|
+
@sector_effects&.update
|
|
107
|
+
@player_state&.update_viewheight
|
|
108
|
+
@player_state&.update_attack # Attack timing at 35fps like DOOM
|
|
109
|
+
@combat&.update
|
|
110
|
+
end
|
|
111
|
+
@animations&.update(@leveltime)
|
|
112
|
+
|
|
113
|
+
# Update HUD animations
|
|
114
|
+
@status_bar&.update
|
|
115
|
+
|
|
116
|
+
# Update sector actions (doors, lifts, etc.)
|
|
117
|
+
if @sector_actions
|
|
118
|
+
@sector_actions.update_player_position(@renderer.player_x, @renderer.player_y)
|
|
119
|
+
@sector_actions.update
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Check item pickups
|
|
123
|
+
if @item_pickup
|
|
124
|
+
@item_pickup.update(@renderer.player_x, @renderer.player_y)
|
|
125
|
+
@renderer.hidden_things = @item_pickup.picked_up
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Pass combat state to renderer for death frame rendering
|
|
129
|
+
@renderer.combat = @combat
|
|
130
|
+
|
|
131
|
+
# Render the 3D world
|
|
33
132
|
@renderer.render_frame
|
|
133
|
+
|
|
134
|
+
# Render HUD on top
|
|
135
|
+
if @weapon_renderer
|
|
136
|
+
@weapon_renderer.render(@renderer.framebuffer)
|
|
137
|
+
end
|
|
138
|
+
if @status_bar
|
|
139
|
+
@status_bar.render(@renderer.framebuffer)
|
|
140
|
+
end
|
|
34
141
|
end
|
|
35
142
|
|
|
36
|
-
def handle_input
|
|
143
|
+
def handle_input(delta_time)
|
|
37
144
|
# Mouse look
|
|
38
145
|
handle_mouse_look
|
|
39
146
|
|
|
@@ -45,83 +152,391 @@ module Doom
|
|
|
45
152
|
@renderer.turn(-TURN_SPEED)
|
|
46
153
|
end
|
|
47
154
|
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
|
|
155
|
+
# Apply thrust from input (P_Thrust: additive, scaled by delta_time)
|
|
156
|
+
thrust = MOVE_THRUST_RATE * delta_time
|
|
157
|
+
has_input = false
|
|
51
158
|
|
|
52
159
|
if Gosu.button_down?(Gosu::KB_UP) || Gosu.button_down?(Gosu::KB_W)
|
|
53
|
-
|
|
54
|
-
|
|
160
|
+
@move_momx += @renderer.cos_angle * thrust
|
|
161
|
+
@move_momy += @renderer.sin_angle * thrust
|
|
162
|
+
has_input = true
|
|
55
163
|
end
|
|
56
164
|
if Gosu.button_down?(Gosu::KB_DOWN) || Gosu.button_down?(Gosu::KB_S)
|
|
57
|
-
|
|
58
|
-
|
|
165
|
+
@move_momx -= @renderer.cos_angle * thrust
|
|
166
|
+
@move_momy -= @renderer.sin_angle * thrust
|
|
167
|
+
has_input = true
|
|
59
168
|
end
|
|
60
|
-
|
|
61
|
-
# Strafe
|
|
62
169
|
if Gosu.button_down?(Gosu::KB_A)
|
|
63
|
-
|
|
64
|
-
|
|
170
|
+
@move_momx -= @renderer.sin_angle * thrust
|
|
171
|
+
@move_momy += @renderer.cos_angle * thrust
|
|
172
|
+
has_input = true
|
|
65
173
|
end
|
|
66
174
|
if Gosu.button_down?(Gosu::KB_D)
|
|
67
|
-
|
|
68
|
-
|
|
175
|
+
@move_momx += @renderer.sin_angle * thrust
|
|
176
|
+
@move_momy -= @renderer.cos_angle * thrust
|
|
177
|
+
has_input = true
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Apply friction (continuous-time equivalent of *= 0.90625 per tic)
|
|
181
|
+
decay = Math.exp(-FRICTION_DECAY_RATE * delta_time)
|
|
182
|
+
if !has_input && @move_momx.abs < STOPSPEED && @move_momy.abs < STOPSPEED
|
|
183
|
+
@move_momx = 0.0
|
|
184
|
+
@move_momy = 0.0
|
|
185
|
+
else
|
|
186
|
+
@move_momx *= decay
|
|
187
|
+
@move_momy *= decay
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Track movement state for weapon/view bob
|
|
191
|
+
if @player_state
|
|
192
|
+
@player_state.is_moving = has_input
|
|
193
|
+
@player_state.set_movement_momentum(@move_momx, @move_momy)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Apply momentum with collision detection (scale by delta_time for frame-rate independence)
|
|
197
|
+
if @move_momx.abs > STOPSPEED || @move_momy.abs > STOPSPEED
|
|
198
|
+
try_move(@move_momx * delta_time, @move_momy * delta_time)
|
|
69
199
|
end
|
|
70
200
|
|
|
71
|
-
#
|
|
72
|
-
if
|
|
73
|
-
|
|
201
|
+
# Handle firing (left click, Z, or Shift - Ctrl conflicts with macOS spaces)
|
|
202
|
+
if @player_state && ((@mouse_captured && Gosu.button_down?(Gosu::MS_LEFT)) ||
|
|
203
|
+
Gosu.button_down?(Gosu::KB_X) || Gosu.button_down?(Gosu::KB_LEFT_SHIFT) ||
|
|
204
|
+
Gosu.button_down?(Gosu::KB_RIGHT_SHIFT))
|
|
205
|
+
was_attacking = @player_state.attacking
|
|
206
|
+
@player_state.start_attack
|
|
207
|
+
# Fire hitscan on the first frame of the attack
|
|
208
|
+
if @player_state.attacking && !was_attacking && @combat
|
|
209
|
+
@combat.fire(@renderer.player_x, @renderer.player_y, @renderer.player_z,
|
|
210
|
+
@renderer.cos_angle, @renderer.sin_angle, @player_state.weapon)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Handle weapon switching with number keys
|
|
215
|
+
handle_weapon_switch if @player_state
|
|
216
|
+
|
|
217
|
+
# Handle use key (spacebar or E)
|
|
218
|
+
handle_use_key if @sector_actions
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def handle_use_key
|
|
222
|
+
use_down = Gosu.button_down?(Gosu::KB_SPACE) || Gosu.button_down?(Gosu::KB_E)
|
|
223
|
+
|
|
224
|
+
if use_down && !@use_pressed
|
|
225
|
+
@use_pressed = true
|
|
226
|
+
try_use_linedef
|
|
227
|
+
elsif !use_down
|
|
228
|
+
@use_pressed = false
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def try_use_linedef
|
|
233
|
+
# Cast a ray forward to find a usable linedef
|
|
234
|
+
player_x = @renderer.player_x
|
|
235
|
+
player_y = @renderer.player_y
|
|
236
|
+
cos_angle = @renderer.cos_angle
|
|
237
|
+
sin_angle = @renderer.sin_angle
|
|
238
|
+
|
|
239
|
+
# Check point in front of player
|
|
240
|
+
use_x = player_x + cos_angle * USE_DISTANCE
|
|
241
|
+
use_y = player_y + sin_angle * USE_DISTANCE
|
|
242
|
+
|
|
243
|
+
# Find the closest linedef the player is facing
|
|
244
|
+
best_linedef = nil
|
|
245
|
+
best_idx = nil
|
|
246
|
+
best_dist = Float::INFINITY
|
|
247
|
+
|
|
248
|
+
@map.linedefs.each_with_index do |linedef, idx|
|
|
249
|
+
next if linedef.special == 0 # Skip non-special linedefs
|
|
250
|
+
|
|
251
|
+
v1 = @map.vertices[linedef.v1]
|
|
252
|
+
v2 = @map.vertices[linedef.v2]
|
|
253
|
+
|
|
254
|
+
# Check if player is close enough to the linedef
|
|
255
|
+
dist = point_to_line_distance(player_x, player_y, v1.x, v1.y, v2.x, v2.y)
|
|
256
|
+
next if dist > USE_DISTANCE
|
|
257
|
+
next if dist >= best_dist
|
|
258
|
+
|
|
259
|
+
# Check if player is facing the linedef (on the front side)
|
|
260
|
+
next unless facing_linedef?(player_x, player_y, cos_angle, sin_angle, v1, v2)
|
|
261
|
+
|
|
262
|
+
best_linedef = linedef
|
|
263
|
+
best_idx = idx
|
|
264
|
+
best_dist = dist
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
if best_linedef
|
|
268
|
+
@sector_actions.use_linedef(best_linedef, best_idx)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def point_to_line_distance(px, py, x1, y1, x2, y2)
|
|
273
|
+
# Vector from line start to point
|
|
274
|
+
dx = px - x1
|
|
275
|
+
dy = py - y1
|
|
276
|
+
|
|
277
|
+
# Line direction vector
|
|
278
|
+
line_dx = x2 - x1
|
|
279
|
+
line_dy = y2 - y1
|
|
280
|
+
line_len_sq = line_dx * line_dx + line_dy * line_dy
|
|
281
|
+
|
|
282
|
+
return Math.sqrt(dx * dx + dy * dy) if line_len_sq == 0
|
|
283
|
+
|
|
284
|
+
# Project point onto line, clamped to segment
|
|
285
|
+
t = ((dx * line_dx) + (dy * line_dy)) / line_len_sq
|
|
286
|
+
t = [[t, 0.0].max, 1.0].min
|
|
287
|
+
|
|
288
|
+
# Closest point on line segment
|
|
289
|
+
closest_x = x1 + t * line_dx
|
|
290
|
+
closest_y = y1 + t * line_dy
|
|
291
|
+
|
|
292
|
+
# Distance from point to closest point on segment
|
|
293
|
+
dist_x = px - closest_x
|
|
294
|
+
dist_y = py - closest_y
|
|
295
|
+
Math.sqrt(dist_x * dist_x + dist_y * dist_y)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def facing_linedef?(px, py, cos_angle, sin_angle, v1, v2)
|
|
299
|
+
# Calculate linedef normal (perpendicular to line, pointing to front side)
|
|
300
|
+
line_dx = v2.x - v1.x
|
|
301
|
+
line_dy = v2.y - v1.y
|
|
302
|
+
|
|
303
|
+
# Normal points to the right of the line direction
|
|
304
|
+
normal_x = -line_dy
|
|
305
|
+
normal_y = line_dx
|
|
306
|
+
|
|
307
|
+
# Normalize
|
|
308
|
+
len = Math.sqrt(normal_x * normal_x + normal_y * normal_y)
|
|
309
|
+
return false if len == 0
|
|
310
|
+
|
|
311
|
+
normal_x /= len
|
|
312
|
+
normal_y /= len
|
|
313
|
+
|
|
314
|
+
# Check if player is on the front side (normal side) of the line
|
|
315
|
+
to_player_x = px - v1.x
|
|
316
|
+
to_player_y = py - v1.y
|
|
317
|
+
dot_player = to_player_x * normal_x + to_player_y * normal_y
|
|
318
|
+
|
|
319
|
+
# Player must be on front side
|
|
320
|
+
return false if dot_player < 0
|
|
321
|
+
|
|
322
|
+
# Check if player is facing toward the line
|
|
323
|
+
dot_facing = cos_angle * (-normal_x) + sin_angle * (-normal_y)
|
|
324
|
+
dot_facing > 0.5 # Must be roughly facing the line
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def handle_weapon_switch
|
|
328
|
+
if Gosu.button_down?(Gosu::KB_1)
|
|
329
|
+
@player_state.switch_weapon(Game::PlayerState::WEAPON_FIST)
|
|
330
|
+
elsif Gosu.button_down?(Gosu::KB_2)
|
|
331
|
+
@player_state.switch_weapon(Game::PlayerState::WEAPON_PISTOL)
|
|
332
|
+
elsif Gosu.button_down?(Gosu::KB_3)
|
|
333
|
+
@player_state.switch_weapon(Game::PlayerState::WEAPON_SHOTGUN)
|
|
334
|
+
elsif Gosu.button_down?(Gosu::KB_4)
|
|
335
|
+
@player_state.switch_weapon(Game::PlayerState::WEAPON_CHAINGUN)
|
|
336
|
+
elsif Gosu.button_down?(Gosu::KB_5)
|
|
337
|
+
@player_state.switch_weapon(Game::PlayerState::WEAPON_ROCKET)
|
|
338
|
+
elsif Gosu.button_down?(Gosu::KB_6)
|
|
339
|
+
@player_state.switch_weapon(Game::PlayerState::WEAPON_PLASMA)
|
|
340
|
+
elsif Gosu.button_down?(Gosu::KB_7)
|
|
341
|
+
@player_state.switch_weapon(Game::PlayerState::WEAPON_BFG)
|
|
74
342
|
end
|
|
75
343
|
end
|
|
76
344
|
|
|
77
345
|
def try_move(dx, dy)
|
|
78
|
-
|
|
79
|
-
|
|
346
|
+
old_x = @renderer.player_x
|
|
347
|
+
old_y = @renderer.player_y
|
|
348
|
+
new_x = old_x + dx
|
|
349
|
+
new_y = old_y + dy
|
|
80
350
|
|
|
81
|
-
# Check if new position is valid
|
|
82
|
-
if
|
|
351
|
+
# Check if new position is valid and path doesn't cross blocking linedefs
|
|
352
|
+
if valid_move?(old_x, old_y, new_x, new_y)
|
|
83
353
|
@renderer.move_to(new_x, new_y)
|
|
84
354
|
update_player_height(new_x, new_y)
|
|
85
355
|
else
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
356
|
+
# Wall sliding: project movement along the blocking wall
|
|
357
|
+
slide_x, slide_y = compute_slide(old_x, old_y, dx, dy)
|
|
358
|
+
if slide_x && (slide_x != 0.0 || slide_y != 0.0)
|
|
359
|
+
sx = old_x + slide_x
|
|
360
|
+
sy = old_y + slide_y
|
|
361
|
+
if valid_move?(old_x, old_y, sx, sy)
|
|
362
|
+
@renderer.move_to(sx, sy)
|
|
363
|
+
update_player_height(sx, sy)
|
|
364
|
+
# Redirect momentum along the wall
|
|
365
|
+
@move_momx = slide_x / ([dx.abs, dy.abs].max.nonzero? || 1) * @move_momx.abs
|
|
366
|
+
@move_momy = slide_y / ([dx.abs, dy.abs].max.nonzero? || 1) * @move_momy.abs
|
|
367
|
+
return
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Fallback: try axis-aligned sliding
|
|
372
|
+
if dx != 0.0 && valid_move?(old_x, old_y, new_x, old_y)
|
|
373
|
+
@renderer.move_to(new_x, old_y)
|
|
374
|
+
update_player_height(new_x, old_y)
|
|
375
|
+
@move_momy *= 0.0
|
|
376
|
+
elsif dy != 0.0 && valid_move?(old_x, old_y, old_x, new_y)
|
|
377
|
+
@renderer.move_to(old_x, new_y)
|
|
378
|
+
update_player_height(old_x, new_y)
|
|
379
|
+
@move_momx *= 0.0
|
|
380
|
+
else
|
|
381
|
+
# Fully blocked - kill momentum
|
|
382
|
+
@move_momx = 0.0
|
|
383
|
+
@move_momy = 0.0
|
|
94
384
|
end
|
|
95
385
|
end
|
|
96
386
|
end
|
|
97
387
|
|
|
388
|
+
# Find the blocking linedef and project movement along it
|
|
389
|
+
def compute_slide(px, py, dx, dy)
|
|
390
|
+
best_wall = nil
|
|
391
|
+
best_dist = Float::INFINITY
|
|
392
|
+
|
|
393
|
+
@map.linedefs.each do |linedef|
|
|
394
|
+
v1 = @map.vertices[linedef.v1]
|
|
395
|
+
v2 = @map.vertices[linedef.v2]
|
|
396
|
+
|
|
397
|
+
# Only check linedefs near the player
|
|
398
|
+
next unless line_circle_intersect?(v1.x, v1.y, v2.x, v2.y, px + dx, py + dy, PLAYER_RADIUS)
|
|
399
|
+
|
|
400
|
+
# Check if this linedef actually blocks
|
|
401
|
+
next unless linedef_blocks?(linedef, px + dx, py + dy) ||
|
|
402
|
+
crosses_blocking_linedef?(px, py, px + dx, py + dy, linedef)
|
|
403
|
+
|
|
404
|
+
# Distance from player to this linedef
|
|
405
|
+
dist = point_to_line_distance(px, py, v1.x, v1.y, v2.x, v2.y)
|
|
406
|
+
if dist < best_dist
|
|
407
|
+
best_dist = dist
|
|
408
|
+
best_wall = linedef
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
return nil unless best_wall
|
|
413
|
+
|
|
414
|
+
# Get wall direction vector
|
|
415
|
+
v1 = @map.vertices[best_wall.v1]
|
|
416
|
+
v2 = @map.vertices[best_wall.v2]
|
|
417
|
+
wall_dx = (v2.x - v1.x).to_f
|
|
418
|
+
wall_dy = (v2.y - v1.y).to_f
|
|
419
|
+
wall_len = Math.sqrt(wall_dx * wall_dx + wall_dy * wall_dy)
|
|
420
|
+
return nil if wall_len == 0
|
|
421
|
+
|
|
422
|
+
wall_dx /= wall_len
|
|
423
|
+
wall_dy /= wall_len
|
|
424
|
+
|
|
425
|
+
# Project movement onto wall direction
|
|
426
|
+
dot = dx * wall_dx + dy * wall_dy
|
|
427
|
+
[dot * wall_dx, dot * wall_dy]
|
|
428
|
+
end
|
|
429
|
+
|
|
98
430
|
def update_player_height(x, y)
|
|
99
431
|
sector = @map.sector_at(x, y)
|
|
100
432
|
return unless sector
|
|
101
433
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
@
|
|
434
|
+
new_floor = sector.floor_height
|
|
435
|
+
|
|
436
|
+
if @player_state
|
|
437
|
+
# Detect step: floor height changed since last move
|
|
438
|
+
if @last_floor_height && @last_floor_height != new_floor
|
|
439
|
+
step = new_floor - @last_floor_height
|
|
440
|
+
@player_state.notify_step(step) if step.abs <= 24
|
|
441
|
+
end
|
|
442
|
+
@last_floor_height = new_floor
|
|
443
|
+
|
|
444
|
+
view_bob = @player_state.view_bob_offset
|
|
445
|
+
@renderer.set_z(new_floor + @player_state.viewheight + view_bob)
|
|
446
|
+
else
|
|
447
|
+
@renderer.set_z(new_floor + 41)
|
|
448
|
+
end
|
|
105
449
|
end
|
|
106
450
|
|
|
107
|
-
def
|
|
108
|
-
# Check if
|
|
109
|
-
sector = @map.sector_at(
|
|
451
|
+
def valid_move?(old_x, old_y, new_x, new_y)
|
|
452
|
+
# Check if destination is inside a valid sector
|
|
453
|
+
sector = @map.sector_at(new_x, new_y)
|
|
110
454
|
return false unless sector
|
|
111
455
|
|
|
112
|
-
# Check floor height - can't
|
|
456
|
+
# Check floor height - can't step up too high
|
|
113
457
|
floor_height = sector.floor_height
|
|
114
458
|
return false if floor_height > @renderer.player_z + 24 # Max step height
|
|
115
459
|
|
|
116
|
-
# Check against blocking linedefs
|
|
460
|
+
# Check against blocking linedefs: both circle intersection and path crossing
|
|
117
461
|
@map.linedefs.each do |linedef|
|
|
118
|
-
|
|
119
|
-
|
|
462
|
+
if linedef_blocks?(linedef, new_x, new_y)
|
|
463
|
+
return false
|
|
464
|
+
end
|
|
465
|
+
if crosses_blocking_linedef?(old_x, old_y, new_x, new_y, linedef)
|
|
466
|
+
return false
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# Check against solid things (monsters, barrels, pillars, etc.)
|
|
471
|
+
combined_radius = PLAYER_RADIUS
|
|
472
|
+
picked = @item_pickup&.picked_up
|
|
473
|
+
@map.things.each_with_index do |thing, idx|
|
|
474
|
+
next if picked && picked[idx]
|
|
475
|
+
next if @combat && @combat.dead?(idx)
|
|
476
|
+
thing_radius = SOLID_THING_RADIUS[thing.type]
|
|
477
|
+
next unless thing_radius
|
|
478
|
+
|
|
479
|
+
dx = new_x - thing.x
|
|
480
|
+
dy = new_y - thing.y
|
|
481
|
+
min_dist = combined_radius + thing_radius
|
|
482
|
+
if dx * dx + dy * dy < min_dist * min_dist
|
|
483
|
+
return false
|
|
484
|
+
end
|
|
120
485
|
end
|
|
121
486
|
|
|
122
487
|
true
|
|
123
488
|
end
|
|
124
489
|
|
|
490
|
+
# Check if movement from (x1,y1) to (x2,y2) crosses a blocking linedef
|
|
491
|
+
def crosses_blocking_linedef?(x1, y1, x2, y2, linedef)
|
|
492
|
+
v1 = @map.vertices[linedef.v1]
|
|
493
|
+
v2 = @map.vertices[linedef.v2]
|
|
494
|
+
|
|
495
|
+
# One-sided linedef always blocks crossing
|
|
496
|
+
if linedef.sidedef_left == 0xFFFF
|
|
497
|
+
return segments_intersect?(x1, y1, x2, y2, v1.x, v1.y, v2.x, v2.y)
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# BLOCKING flag blocks crossing even on two-sided linedefs
|
|
501
|
+
if (linedef.flags & 0x0001) != 0
|
|
502
|
+
return segments_intersect?(x1, y1, x2, y2, v1.x, v1.y, v2.x, v2.y)
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# Two-sided: check if impassable (high step OR low ceiling)
|
|
506
|
+
front_side = @map.sidedefs[linedef.sidedef_right]
|
|
507
|
+
back_side = @map.sidedefs[linedef.sidedef_left]
|
|
508
|
+
front_sector = @map.sectors[front_side.sector]
|
|
509
|
+
back_sector = @map.sectors[back_side.sector]
|
|
510
|
+
|
|
511
|
+
step = (back_sector.floor_height - front_sector.floor_height).abs
|
|
512
|
+
min_ceiling = [front_sector.ceiling_height, back_sector.ceiling_height].min
|
|
513
|
+
max_floor = [front_sector.floor_height, back_sector.floor_height].max
|
|
514
|
+
|
|
515
|
+
# Passable if step is small AND enough headroom
|
|
516
|
+
return false if step <= 24 && (min_ceiling - max_floor) >= 56
|
|
517
|
+
|
|
518
|
+
segments_intersect?(x1, y1, x2, y2, v1.x, v1.y, v2.x, v2.y)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
# Test if line segment (ax1,ay1)-(ax2,ay2) intersects (bx1,by1)-(bx2,by2)
|
|
522
|
+
def segments_intersect?(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2)
|
|
523
|
+
d1x = ax2 - ax1
|
|
524
|
+
d1y = ay2 - ay1
|
|
525
|
+
d2x = bx2 - bx1
|
|
526
|
+
d2y = by2 - by1
|
|
527
|
+
|
|
528
|
+
denom = d1x * d2y - d1y * d2x
|
|
529
|
+
return false if denom.abs < 0.001 # Parallel
|
|
530
|
+
|
|
531
|
+
dx = bx1 - ax1
|
|
532
|
+
dy = by1 - ay1
|
|
533
|
+
|
|
534
|
+
t = (dx * d2y - dy * d2x).to_f / denom
|
|
535
|
+
u = (dx * d1y - dy * d1x).to_f / denom
|
|
536
|
+
|
|
537
|
+
t > 0.0 && t < 1.0 && u >= 0.0 && u <= 1.0
|
|
538
|
+
end
|
|
539
|
+
|
|
125
540
|
def linedef_blocks?(linedef, x, y)
|
|
126
541
|
v1 = @map.vertices[linedef.v1]
|
|
127
542
|
v2 = @map.vertices[linedef.v2]
|
|
@@ -132,23 +547,22 @@ module Doom
|
|
|
132
547
|
# One-sided linedef (wall) always blocks
|
|
133
548
|
return true if linedef.sidedef_left == 0xFFFF
|
|
134
549
|
|
|
135
|
-
#
|
|
550
|
+
# BLOCKING flag (0x0001) blocks even on two-sided linedefs (e.g., windows)
|
|
551
|
+
return true if (linedef.flags & 0x0001) != 0
|
|
552
|
+
|
|
553
|
+
# Two-sided: check if impassable (high step OR low ceiling)
|
|
136
554
|
front_side = @map.sidedefs[linedef.sidedef_right]
|
|
137
555
|
back_side = @map.sidedefs[linedef.sidedef_left]
|
|
138
556
|
|
|
139
557
|
front_sector = @map.sectors[front_side.sector]
|
|
140
558
|
back_sector = @map.sectors[back_side.sector]
|
|
141
559
|
|
|
142
|
-
|
|
143
|
-
step = back_sector.floor_height - front_sector.floor_height
|
|
144
|
-
return true if step.abs > 24
|
|
145
|
-
|
|
146
|
-
# Check ceiling clearance
|
|
560
|
+
step = (back_sector.floor_height - front_sector.floor_height).abs
|
|
147
561
|
min_ceiling = [front_sector.ceiling_height, back_sector.ceiling_height].min
|
|
148
562
|
max_floor = [front_sector.floor_height, back_sector.floor_height].max
|
|
149
|
-
return true if min_ceiling - max_floor < 56 # Player height
|
|
150
563
|
|
|
151
|
-
|
|
564
|
+
# Block if step too high OR not enough headroom
|
|
565
|
+
step > 24 || (min_ceiling - max_floor) < 56
|
|
152
566
|
end
|
|
153
567
|
|
|
154
568
|
def line_circle_intersect?(x1, y1, x2, y2, cx, cy, radius)
|
|
@@ -199,16 +613,46 @@ module Doom
|
|
|
199
613
|
end
|
|
200
614
|
|
|
201
615
|
def draw
|
|
202
|
-
|
|
203
|
-
|
|
616
|
+
if @show_map
|
|
617
|
+
draw_automap
|
|
618
|
+
else
|
|
619
|
+
# Fast RGBA conversion using pre-built palette
|
|
620
|
+
rgba = @renderer.framebuffer.map { |idx| @palette_rgba[idx] }.join
|
|
621
|
+
|
|
622
|
+
@screen_image = Gosu::Image.from_blob(
|
|
623
|
+
Render::SCREEN_WIDTH,
|
|
624
|
+
Render::SCREEN_HEIGHT,
|
|
625
|
+
rgba
|
|
626
|
+
)
|
|
204
627
|
|
|
205
|
-
|
|
206
|
-
Render::SCREEN_WIDTH,
|
|
207
|
-
Render::SCREEN_HEIGHT,
|
|
208
|
-
rgba
|
|
209
|
-
)
|
|
628
|
+
@screen_image.draw(0, 0, 0, SCALE, SCALE)
|
|
210
629
|
|
|
211
|
-
|
|
630
|
+
draw_debug_overlay if @show_debug
|
|
631
|
+
end
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def draw_debug_overlay
|
|
635
|
+
sector = @map.sector_at(@renderer.player_x, @renderer.player_y)
|
|
636
|
+
return unless sector
|
|
637
|
+
|
|
638
|
+
# Find sector index
|
|
639
|
+
sector_idx = @map.sectors.index(sector)
|
|
640
|
+
|
|
641
|
+
lines = [
|
|
642
|
+
"Sector #{sector_idx}",
|
|
643
|
+
"Floor: #{sector.floor_height} (#{sector.floor_texture})",
|
|
644
|
+
"Ceil: #{sector.ceiling_height} (#{sector.ceiling_texture})",
|
|
645
|
+
"Light: #{sector.light_level}",
|
|
646
|
+
"Pos: #{@renderer.player_x.round}, #{@renderer.player_y.round}",
|
|
647
|
+
"Heading: #{(Math.atan2(@renderer.sin_angle, @renderer.cos_angle) * 180.0 / Math::PI).round(1)}",
|
|
648
|
+
]
|
|
649
|
+
|
|
650
|
+
y = 4
|
|
651
|
+
lines.each do |line|
|
|
652
|
+
@debug_font.draw_text(line, 6, y + 1, 1, 1, 1, Gosu::Color::BLACK)
|
|
653
|
+
@debug_font.draw_text(line, 5, y, 1, 1, 1, Gosu::Color::WHITE)
|
|
654
|
+
y += 18
|
|
655
|
+
end
|
|
212
656
|
end
|
|
213
657
|
|
|
214
658
|
def button_down(id)
|
|
@@ -226,12 +670,215 @@ module Doom
|
|
|
226
670
|
@mouse_captured = true
|
|
227
671
|
@last_mouse_x = mouse_x
|
|
228
672
|
end
|
|
673
|
+
when Gosu::KB_Z
|
|
674
|
+
@show_debug = !@show_debug
|
|
675
|
+
when Gosu::KB_M
|
|
676
|
+
@show_map = !@show_map
|
|
677
|
+
when Gosu::KB_F12
|
|
678
|
+
capture_debug_snapshot
|
|
229
679
|
end
|
|
230
680
|
end
|
|
231
681
|
|
|
232
682
|
def needs_cursor?
|
|
233
683
|
!@mouse_captured
|
|
234
684
|
end
|
|
685
|
+
|
|
686
|
+
# --- Debug Snapshot ---
|
|
687
|
+
|
|
688
|
+
def capture_debug_snapshot
|
|
689
|
+
dir = File.join(File.expand_path('../..', __dir__), '..', 'screenshots')
|
|
690
|
+
FileUtils.mkdir_p(dir)
|
|
691
|
+
|
|
692
|
+
ts = Time.now.strftime('%Y%m%d_%H%M%S_%L')
|
|
693
|
+
prefix = File.join(dir, ts)
|
|
694
|
+
|
|
695
|
+
# Save framebuffer as PNG
|
|
696
|
+
require 'chunky_png' unless defined?(ChunkyPNG)
|
|
697
|
+
w = Render::SCREEN_WIDTH
|
|
698
|
+
h = Render::SCREEN_HEIGHT
|
|
699
|
+
img = ChunkyPNG::Image.new(w, h)
|
|
700
|
+
fb = @renderer.framebuffer
|
|
701
|
+
colors = @palette.colors
|
|
702
|
+
h.times do |y|
|
|
703
|
+
row = y * w
|
|
704
|
+
w.times do |x|
|
|
705
|
+
r, g, b = colors[fb[row + x]]
|
|
706
|
+
img[x, y] = ChunkyPNG::Color.rgb(r, g, b)
|
|
707
|
+
end
|
|
708
|
+
end
|
|
709
|
+
img.save("#{prefix}.png")
|
|
710
|
+
|
|
711
|
+
# Save player state and sector info
|
|
712
|
+
sector = @map.sector_at(@renderer.player_x, @renderer.player_y)
|
|
713
|
+
sector_idx = sector ? @map.sectors.index(sector) : nil
|
|
714
|
+
angle_deg = Math.atan2(@renderer.sin_angle, @renderer.cos_angle) * 180.0 / Math::PI
|
|
715
|
+
|
|
716
|
+
# Sprite diagnostics
|
|
717
|
+
sprites_info = @renderer.sprite_diagnostics
|
|
718
|
+
nearby = sprites_info.select { |s| s[:dist] && s[:dist] < 1500 }
|
|
719
|
+
.sort_by { |s| s[:dist] }
|
|
720
|
+
|
|
721
|
+
sprite_lines = nearby.map do |s|
|
|
722
|
+
" #{s[:prefix]} type=#{s[:type]} pos=(#{s[:x]},#{s[:y]}) dist=#{s[:dist]} " \
|
|
723
|
+
"screen_x=#{s[:screen_x]} scale=#{s[:sprite_scale]} " \
|
|
724
|
+
"range=#{s[:screen_range]} status=#{s[:status]} " \
|
|
725
|
+
"clip_segs=#{s[:clipping_segs]}" \
|
|
726
|
+
"#{s[:clipping_detail]&.any? ? "\n clips: #{s[:clipping_detail].map { |c| "ds[#{c[:x1]}..#{c[:x2]}] scale=#{c[:scale]} sil=#{c[:sil]}" }.join(', ')}" : ''}"
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
File.write("#{prefix}.txt", <<~INFO)
|
|
730
|
+
pos: #{@renderer.player_x.round(1)}, #{@renderer.player_y.round(1)}, #{@renderer.player_z.round(1)}
|
|
731
|
+
angle: #{angle_deg.round(1)}
|
|
732
|
+
sector: #{sector_idx}
|
|
733
|
+
floor: #{sector&.floor_height} (#{sector&.floor_texture})
|
|
734
|
+
ceil: #{sector&.ceiling_height} (#{sector&.ceiling_texture})
|
|
735
|
+
light: #{sector&.light_level}
|
|
736
|
+
|
|
737
|
+
nearby sprites (#{nearby.size}):
|
|
738
|
+
#{sprite_lines.join("\n")}
|
|
739
|
+
INFO
|
|
740
|
+
|
|
741
|
+
puts "Snapshot saved: #{prefix}.png + .txt"
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
# --- Automap ---
|
|
745
|
+
|
|
746
|
+
MAP_MARGIN = 20
|
|
747
|
+
|
|
748
|
+
def build_sector_colors
|
|
749
|
+
# Generate distinct colors for each sector using golden ratio hue spacing
|
|
750
|
+
num_sectors = @map.sectors.size
|
|
751
|
+
colors = Array.new(num_sectors)
|
|
752
|
+
phi = (1 + Math.sqrt(5)) / 2.0
|
|
753
|
+
|
|
754
|
+
num_sectors.times do |i|
|
|
755
|
+
hue = (i * phi * 360) % 360
|
|
756
|
+
colors[i] = hsv_to_gosu(hue, 0.6, 0.85)
|
|
757
|
+
end
|
|
758
|
+
colors
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
def hsv_to_gosu(h, s, v)
|
|
762
|
+
c = v * s
|
|
763
|
+
x = c * (1 - ((h / 60.0) % 2 - 1).abs)
|
|
764
|
+
m = v - c
|
|
765
|
+
|
|
766
|
+
r, g, b = case (h / 60).to_i % 6
|
|
767
|
+
when 0 then [c, x, 0]
|
|
768
|
+
when 1 then [x, c, 0]
|
|
769
|
+
when 2 then [0, c, x]
|
|
770
|
+
when 3 then [0, x, c]
|
|
771
|
+
when 4 then [x, 0, c]
|
|
772
|
+
when 5 then [c, 0, x]
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
Gosu::Color.new(255, ((r + m) * 255).to_i, ((g + m) * 255).to_i, ((b + m) * 255).to_i)
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
def draw_automap
|
|
779
|
+
# Black background
|
|
780
|
+
Gosu.draw_rect(0, 0, width, height, Gosu::Color::BLACK, 0)
|
|
781
|
+
|
|
782
|
+
# Compute map bounds
|
|
783
|
+
verts = @map.vertices
|
|
784
|
+
min_x = min_y = Float::INFINITY
|
|
785
|
+
max_x = max_y = -Float::INFINITY
|
|
786
|
+
verts.each do |v|
|
|
787
|
+
min_x = v.x if v.x < min_x
|
|
788
|
+
max_x = v.x if v.x > max_x
|
|
789
|
+
min_y = v.y if v.y < min_y
|
|
790
|
+
max_y = v.y if v.y > max_y
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
map_w = max_x - min_x
|
|
794
|
+
map_h = max_y - min_y
|
|
795
|
+
return if map_w == 0 || map_h == 0
|
|
796
|
+
|
|
797
|
+
# Scale to fit screen with margin
|
|
798
|
+
draw_w = width - MAP_MARGIN * 2
|
|
799
|
+
draw_h = height - MAP_MARGIN * 2
|
|
800
|
+
scale = [draw_w.to_f / map_w, draw_h.to_f / map_h].min
|
|
801
|
+
|
|
802
|
+
# Center the map
|
|
803
|
+
offset_x = MAP_MARGIN + (draw_w - map_w * scale) / 2.0
|
|
804
|
+
offset_y = MAP_MARGIN + (draw_h - map_h * scale) / 2.0
|
|
805
|
+
|
|
806
|
+
# World to screen coordinate transform (Y flipped: world Y+ is up, screen Y+ is down)
|
|
807
|
+
to_sx = ->(wx) { offset_x + (wx - min_x) * scale }
|
|
808
|
+
to_sy = ->(wy) { offset_y + (max_y - wy) * scale }
|
|
809
|
+
|
|
810
|
+
# Draw linedefs colored by front sector
|
|
811
|
+
two_sided_color = Gosu::Color.new(100, 80, 80, 80)
|
|
812
|
+
|
|
813
|
+
@map.linedefs.each do |linedef|
|
|
814
|
+
v1 = verts[linedef.v1]
|
|
815
|
+
v2 = verts[linedef.v2]
|
|
816
|
+
sx1 = to_sx.call(v1.x)
|
|
817
|
+
sy1 = to_sy.call(v1.y)
|
|
818
|
+
sx2 = to_sx.call(v2.x)
|
|
819
|
+
sy2 = to_sy.call(v2.y)
|
|
820
|
+
|
|
821
|
+
if linedef.two_sided?
|
|
822
|
+
# Two-sided: dim line, colored by front sector
|
|
823
|
+
front_sd = @map.sidedefs[linedef.sidedef_right]
|
|
824
|
+
color = @sector_colors[front_sd.sector]
|
|
825
|
+
dim = Gosu::Color.new(100, color.red, color.green, color.blue)
|
|
826
|
+
Gosu.draw_line(sx1, sy1, dim, sx2, sy2, dim, 1)
|
|
827
|
+
else
|
|
828
|
+
# One-sided: solid wall, bright sector color
|
|
829
|
+
front_sd = @map.sidedefs[linedef.sidedef_right]
|
|
830
|
+
color = @sector_colors[front_sd.sector]
|
|
831
|
+
Gosu.draw_line(sx1, sy1, color, sx2, sy2, color, 1)
|
|
832
|
+
end
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
# Draw player
|
|
836
|
+
px = to_sx.call(@renderer.player_x)
|
|
837
|
+
py = to_sy.call(@renderer.player_y)
|
|
838
|
+
|
|
839
|
+
cos_a = @renderer.cos_angle
|
|
840
|
+
sin_a = @renderer.sin_angle
|
|
841
|
+
|
|
842
|
+
# FOV cone
|
|
843
|
+
fov_len = 40.0
|
|
844
|
+
half_fov = Math::PI / 4.0 # 45 deg half = 90 deg total
|
|
845
|
+
|
|
846
|
+
# Cone edges (in world space, Y+ is up; on screen Y is flipped via to_sy)
|
|
847
|
+
left_dx = Math.cos(half_fov) * cos_a - Math.sin(half_fov) * sin_a
|
|
848
|
+
left_dy = Math.cos(half_fov) * sin_a + Math.sin(half_fov) * cos_a
|
|
849
|
+
right_dx = Math.cos(-half_fov) * cos_a - Math.sin(-half_fov) * sin_a
|
|
850
|
+
right_dy = Math.cos(-half_fov) * sin_a + Math.sin(-half_fov) * cos_a
|
|
851
|
+
|
|
852
|
+
# Screen positions for cone tips
|
|
853
|
+
lx = px + left_dx * fov_len
|
|
854
|
+
ly = py - left_dy * fov_len # negate because screen Y is flipped
|
|
855
|
+
rx = px + right_dx * fov_len
|
|
856
|
+
ry = py - right_dy * fov_len
|
|
857
|
+
|
|
858
|
+
cone_color = Gosu::Color.new(60, 0, 255, 0)
|
|
859
|
+
Gosu.draw_triangle(px, py, cone_color, lx, ly, cone_color, rx, ry, cone_color, 2)
|
|
860
|
+
|
|
861
|
+
# Cone edge lines
|
|
862
|
+
edge_color = Gosu::Color.new(180, 0, 255, 0)
|
|
863
|
+
Gosu.draw_line(px, py, edge_color, lx, ly, edge_color, 3)
|
|
864
|
+
Gosu.draw_line(px, py, edge_color, rx, ry, edge_color, 3)
|
|
865
|
+
|
|
866
|
+
# Player dot
|
|
867
|
+
dot_size = 4
|
|
868
|
+
Gosu.draw_rect(px - dot_size, py - dot_size, dot_size * 2, dot_size * 2, Gosu::Color::GREEN, 3)
|
|
869
|
+
|
|
870
|
+
# Direction line
|
|
871
|
+
dir_len = 12.0
|
|
872
|
+
dx = px + cos_a * dir_len
|
|
873
|
+
dy = py - sin_a * dir_len
|
|
874
|
+
Gosu.draw_line(px, py, Gosu::Color::WHITE, dx, dy, Gosu::Color::WHITE, 3)
|
|
875
|
+
end
|
|
876
|
+
|
|
877
|
+
# --- End Automap ---
|
|
878
|
+
|
|
879
|
+
def needs_cursor?
|
|
880
|
+
!@mouse_captured
|
|
881
|
+
end
|
|
235
882
|
end
|
|
236
883
|
end
|
|
237
884
|
end
|