doom 0.4.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 +4 -4
- data/README.md +44 -22
- data/bin/doom +5 -3
- data/lib/doom/game/animations.rb +97 -0
- data/lib/doom/game/combat.rb +370 -0
- data/lib/doom/game/item_pickup.rb +170 -0
- data/lib/doom/game/monster_ai.rb +295 -0
- data/lib/doom/game/player_state.rb +144 -9
- data/lib/doom/game/sector_effects.rb +179 -0
- data/lib/doom/platform/gosu_window.rb +673 -71
- data/lib/doom/render/renderer.rb +346 -90
- data/lib/doom/render/status_bar.rb +74 -22
- data/lib/doom/render/weapon_renderer.rb +25 -28
- data/lib/doom/version.rb +1 -1
- data/lib/doom/wad/hud_graphics.rb +70 -2
- data/lib/doom/wad/sprite.rb +98 -24
- data/lib/doom/wad/texture.rb +23 -12
- data/lib/doom/wad_downloader.rb +10 -1
- data/lib/doom.rb +15 -2
- metadata +9 -7
|
@@ -7,17 +7,52 @@ 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
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
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, monster_ai = nil)
|
|
52
|
+
fullscreen = ARGV.include?('--fullscreen') || ARGV.include?('-f')
|
|
53
|
+
super(Render::SCREEN_WIDTH * SCALE, Render::SCREEN_HEIGHT * SCALE, fullscreen)
|
|
20
54
|
self.caption = 'Doom Ruby'
|
|
55
|
+
self.update_interval = 0 # Uncap framerate (default 16.67ms = 60 FPS cap)
|
|
21
56
|
|
|
22
57
|
@renderer = renderer
|
|
23
58
|
@palette = palette
|
|
@@ -26,14 +61,39 @@ module Doom
|
|
|
26
61
|
@status_bar = status_bar
|
|
27
62
|
@weapon_renderer = weapon_renderer
|
|
28
63
|
@sector_actions = sector_actions
|
|
64
|
+
@animations = animations
|
|
65
|
+
@sector_effects = sector_effects
|
|
66
|
+
@item_pickup = item_pickup
|
|
67
|
+
@combat = combat
|
|
68
|
+
@monster_ai = monster_ai
|
|
69
|
+
@last_floor_height = nil
|
|
70
|
+
@move_momx = 0.0
|
|
71
|
+
@move_momy = 0.0
|
|
72
|
+
@leveltime = 0
|
|
73
|
+
@tic_accumulator = 0.0
|
|
29
74
|
@screen_image = nil
|
|
30
75
|
@mouse_captured = false
|
|
31
76
|
@last_mouse_x = nil
|
|
32
77
|
@last_update_time = Time.now
|
|
33
78
|
@use_pressed = false
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@
|
|
79
|
+
@show_debug = false
|
|
80
|
+
@show_map = false
|
|
81
|
+
@debug_font = Gosu::Font.new(16)
|
|
82
|
+
@fps_frames = 0
|
|
83
|
+
@fps_time = Time.now
|
|
84
|
+
@fps_display = 0.0
|
|
85
|
+
|
|
86
|
+
# Precompute sector colors for automap
|
|
87
|
+
@sector_colors = build_sector_colors
|
|
88
|
+
|
|
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]
|
|
37
97
|
end
|
|
38
98
|
|
|
39
99
|
def update
|
|
@@ -42,14 +102,39 @@ module Doom
|
|
|
42
102
|
delta_time = now - @last_update_time
|
|
43
103
|
@last_update_time = now
|
|
44
104
|
|
|
45
|
-
handle_input
|
|
105
|
+
handle_input(delta_time)
|
|
46
106
|
|
|
47
|
-
# Update player state
|
|
107
|
+
# Update player state (per-frame for smooth bob)
|
|
48
108
|
if @player_state
|
|
49
|
-
@player_state.update_attack
|
|
50
109
|
@player_state.update_bob(delta_time)
|
|
110
|
+
@player_state.update_view_bob(delta_time)
|
|
51
111
|
end
|
|
52
112
|
|
|
113
|
+
# Advance game tics at 35/sec (DOOM's tic rate)
|
|
114
|
+
@tic_accumulator += delta_time * 35.0
|
|
115
|
+
while @tic_accumulator >= 1.0
|
|
116
|
+
@leveltime += 1
|
|
117
|
+
@tic_accumulator -= 1.0
|
|
118
|
+
@sector_effects&.update
|
|
119
|
+
@player_state&.update_viewheight
|
|
120
|
+
@player_state&.update_attack # Attack timing at 35fps like DOOM
|
|
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
|
|
135
|
+
end
|
|
136
|
+
@animations&.update(@leveltime)
|
|
137
|
+
|
|
53
138
|
# Update HUD animations
|
|
54
139
|
@status_bar&.update
|
|
55
140
|
|
|
@@ -59,19 +144,46 @@ module Doom
|
|
|
59
144
|
@sector_actions.update
|
|
60
145
|
end
|
|
61
146
|
|
|
147
|
+
# Check item pickups
|
|
148
|
+
if @item_pickup
|
|
149
|
+
@item_pickup.update(@renderer.player_x, @renderer.player_y)
|
|
150
|
+
@renderer.hidden_things = @item_pickup.picked_up
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Pass combat state to renderer for death frame rendering
|
|
154
|
+
@renderer.combat = @combat
|
|
155
|
+
@renderer.monster_ai = @monster_ai
|
|
156
|
+
@renderer.leveltime = @leveltime
|
|
157
|
+
|
|
62
158
|
# Render the 3D world
|
|
63
159
|
@renderer.render_frame
|
|
64
160
|
|
|
65
161
|
# Render HUD on top
|
|
66
|
-
if @weapon_renderer
|
|
162
|
+
if @weapon_renderer && !@player_state&.dead
|
|
67
163
|
@weapon_renderer.render(@renderer.framebuffer)
|
|
68
164
|
end
|
|
69
165
|
if @status_bar
|
|
70
166
|
@status_bar.render(@renderer.framebuffer)
|
|
71
167
|
end
|
|
168
|
+
|
|
169
|
+
# Red tint when dead
|
|
170
|
+
if @player_state&.dead
|
|
171
|
+
apply_death_tint(@renderer.framebuffer)
|
|
172
|
+
end
|
|
72
173
|
end
|
|
73
174
|
|
|
74
|
-
def handle_input
|
|
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
|
+
|
|
75
187
|
# Mouse look
|
|
76
188
|
handle_mouse_look
|
|
77
189
|
|
|
@@ -83,41 +195,63 @@ module Doom
|
|
|
83
195
|
@renderer.turn(-TURN_SPEED)
|
|
84
196
|
end
|
|
85
197
|
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
198
|
+
# Apply thrust from input (P_Thrust: additive, scaled by delta_time)
|
|
199
|
+
thrust = MOVE_THRUST_RATE * delta_time
|
|
200
|
+
has_input = false
|
|
89
201
|
|
|
90
202
|
if Gosu.button_down?(Gosu::KB_UP) || Gosu.button_down?(Gosu::KB_W)
|
|
91
|
-
|
|
92
|
-
|
|
203
|
+
@move_momx += @renderer.cos_angle * thrust
|
|
204
|
+
@move_momy += @renderer.sin_angle * thrust
|
|
205
|
+
has_input = true
|
|
93
206
|
end
|
|
94
207
|
if Gosu.button_down?(Gosu::KB_DOWN) || Gosu.button_down?(Gosu::KB_S)
|
|
95
|
-
|
|
96
|
-
|
|
208
|
+
@move_momx -= @renderer.cos_angle * thrust
|
|
209
|
+
@move_momy -= @renderer.sin_angle * thrust
|
|
210
|
+
has_input = true
|
|
97
211
|
end
|
|
98
|
-
|
|
99
|
-
# Strafe
|
|
100
212
|
if Gosu.button_down?(Gosu::KB_A)
|
|
101
|
-
|
|
102
|
-
|
|
213
|
+
@move_momx -= @renderer.sin_angle * thrust
|
|
214
|
+
@move_momy += @renderer.cos_angle * thrust
|
|
215
|
+
has_input = true
|
|
103
216
|
end
|
|
104
217
|
if Gosu.button_down?(Gosu::KB_D)
|
|
105
|
-
|
|
106
|
-
|
|
218
|
+
@move_momx += @renderer.sin_angle * thrust
|
|
219
|
+
@move_momy -= @renderer.cos_angle * thrust
|
|
220
|
+
has_input = true
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Apply friction (continuous-time equivalent of *= 0.90625 per tic)
|
|
224
|
+
decay = Math.exp(-FRICTION_DECAY_RATE * delta_time)
|
|
225
|
+
if !has_input && @move_momx.abs < STOPSPEED && @move_momy.abs < STOPSPEED
|
|
226
|
+
@move_momx = 0.0
|
|
227
|
+
@move_momy = 0.0
|
|
228
|
+
else
|
|
229
|
+
@move_momx *= decay
|
|
230
|
+
@move_momy *= decay
|
|
107
231
|
end
|
|
108
232
|
|
|
109
|
-
# Track
|
|
110
|
-
|
|
111
|
-
|
|
233
|
+
# Track movement state for weapon/view bob
|
|
234
|
+
if @player_state
|
|
235
|
+
@player_state.is_moving = has_input
|
|
236
|
+
@player_state.set_movement_momentum(@move_momx, @move_momy)
|
|
237
|
+
end
|
|
112
238
|
|
|
113
|
-
# Apply
|
|
114
|
-
if
|
|
115
|
-
try_move(
|
|
239
|
+
# Apply momentum with collision detection (scale by delta_time for frame-rate independence)
|
|
240
|
+
if @move_momx.abs > STOPSPEED || @move_momy.abs > STOPSPEED
|
|
241
|
+
try_move(@move_momx * delta_time, @move_momy * delta_time)
|
|
116
242
|
end
|
|
117
243
|
|
|
118
|
-
# Handle firing
|
|
119
|
-
if @player_state && @mouse_captured && Gosu.button_down?(Gosu::MS_LEFT)
|
|
244
|
+
# Handle firing (left click, Z, or Shift - Ctrl conflicts with macOS spaces)
|
|
245
|
+
if @player_state && ((@mouse_captured && Gosu.button_down?(Gosu::MS_LEFT)) ||
|
|
246
|
+
Gosu.button_down?(Gosu::KB_X) || Gosu.button_down?(Gosu::KB_LEFT_SHIFT) ||
|
|
247
|
+
Gosu.button_down?(Gosu::KB_RIGHT_SHIFT))
|
|
248
|
+
was_attacking = @player_state.attacking
|
|
120
249
|
@player_state.start_attack
|
|
250
|
+
# Fire hitscan on the first frame of the attack
|
|
251
|
+
if @player_state.attacking && !was_attacking && @combat
|
|
252
|
+
@combat.fire(@renderer.player_x, @renderer.player_y, @renderer.player_z,
|
|
253
|
+
@renderer.cos_angle, @renderer.sin_angle, @player_state.weapon)
|
|
254
|
+
end
|
|
121
255
|
end
|
|
122
256
|
|
|
123
257
|
# Handle weapon switching with number keys
|
|
@@ -252,53 +386,200 @@ module Doom
|
|
|
252
386
|
end
|
|
253
387
|
|
|
254
388
|
def try_move(dx, dy)
|
|
255
|
-
|
|
256
|
-
|
|
389
|
+
old_x = @renderer.player_x
|
|
390
|
+
old_y = @renderer.player_y
|
|
391
|
+
new_x = old_x + dx
|
|
392
|
+
new_y = old_y + dy
|
|
257
393
|
|
|
258
|
-
# Check if new position is valid
|
|
259
|
-
if
|
|
394
|
+
# Check if new position is valid and path doesn't cross blocking linedefs
|
|
395
|
+
if valid_move?(old_x, old_y, new_x, new_y)
|
|
260
396
|
@renderer.move_to(new_x, new_y)
|
|
261
397
|
update_player_height(new_x, new_y)
|
|
262
398
|
else
|
|
263
|
-
#
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
399
|
+
# Wall sliding: project movement along the blocking wall
|
|
400
|
+
slide_x, slide_y = compute_slide(old_x, old_y, dx, dy)
|
|
401
|
+
if slide_x && (slide_x != 0.0 || slide_y != 0.0)
|
|
402
|
+
sx = old_x + slide_x
|
|
403
|
+
sy = old_y + slide_y
|
|
404
|
+
if valid_move?(old_x, old_y, sx, sy)
|
|
405
|
+
@renderer.move_to(sx, sy)
|
|
406
|
+
update_player_height(sx, sy)
|
|
407
|
+
# Redirect momentum along the wall
|
|
408
|
+
@move_momx = slide_x / ([dx.abs, dy.abs].max.nonzero? || 1) * @move_momx.abs
|
|
409
|
+
@move_momy = slide_y / ([dx.abs, dy.abs].max.nonzero? || 1) * @move_momy.abs
|
|
410
|
+
return
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Fallback: try axis-aligned sliding
|
|
415
|
+
if dx != 0.0 && valid_move?(old_x, old_y, new_x, old_y)
|
|
416
|
+
@renderer.move_to(new_x, old_y)
|
|
417
|
+
update_player_height(new_x, old_y)
|
|
418
|
+
@move_momy *= 0.0
|
|
419
|
+
elsif dy != 0.0 && valid_move?(old_x, old_y, old_x, new_y)
|
|
420
|
+
@renderer.move_to(old_x, new_y)
|
|
421
|
+
update_player_height(old_x, new_y)
|
|
422
|
+
@move_momx *= 0.0
|
|
423
|
+
else
|
|
424
|
+
# Fully blocked - kill momentum
|
|
425
|
+
@move_momx = 0.0
|
|
426
|
+
@move_momy = 0.0
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Find the blocking linedef and project movement along it
|
|
432
|
+
def compute_slide(px, py, dx, dy)
|
|
433
|
+
best_wall = nil
|
|
434
|
+
best_dist = Float::INFINITY
|
|
435
|
+
|
|
436
|
+
@map.linedefs.each do |linedef|
|
|
437
|
+
v1 = @map.vertices[linedef.v1]
|
|
438
|
+
v2 = @map.vertices[linedef.v2]
|
|
439
|
+
|
|
440
|
+
# Only check linedefs near the player
|
|
441
|
+
next unless line_circle_intersect?(v1.x, v1.y, v2.x, v2.y, px + dx, py + dy, PLAYER_RADIUS)
|
|
442
|
+
|
|
443
|
+
# Check if this linedef actually blocks
|
|
444
|
+
next unless linedef_blocks?(linedef, px + dx, py + dy) ||
|
|
445
|
+
crosses_blocking_linedef?(px, py, px + dx, py + dy, linedef)
|
|
446
|
+
|
|
447
|
+
# Distance from player to this linedef
|
|
448
|
+
dist = point_to_line_distance(px, py, v1.x, v1.y, v2.x, v2.y)
|
|
449
|
+
if dist < best_dist
|
|
450
|
+
best_dist = dist
|
|
451
|
+
best_wall = linedef
|
|
271
452
|
end
|
|
272
453
|
end
|
|
454
|
+
|
|
455
|
+
return nil unless best_wall
|
|
456
|
+
|
|
457
|
+
# Get wall direction vector
|
|
458
|
+
v1 = @map.vertices[best_wall.v1]
|
|
459
|
+
v2 = @map.vertices[best_wall.v2]
|
|
460
|
+
wall_dx = (v2.x - v1.x).to_f
|
|
461
|
+
wall_dy = (v2.y - v1.y).to_f
|
|
462
|
+
wall_len = Math.sqrt(wall_dx * wall_dx + wall_dy * wall_dy)
|
|
463
|
+
return nil if wall_len == 0
|
|
464
|
+
|
|
465
|
+
wall_dx /= wall_len
|
|
466
|
+
wall_dy /= wall_len
|
|
467
|
+
|
|
468
|
+
# Project movement onto wall direction
|
|
469
|
+
dot = dx * wall_dx + dy * wall_dy
|
|
470
|
+
[dot * wall_dx, dot * wall_dy]
|
|
273
471
|
end
|
|
274
472
|
|
|
275
473
|
def update_player_height(x, y)
|
|
276
474
|
sector = @map.sector_at(x, y)
|
|
277
475
|
return unless sector
|
|
278
476
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
@
|
|
477
|
+
new_floor = sector.floor_height
|
|
478
|
+
|
|
479
|
+
if @player_state
|
|
480
|
+
# Detect step: floor height changed since last move
|
|
481
|
+
if @last_floor_height && @last_floor_height != new_floor
|
|
482
|
+
step = new_floor - @last_floor_height
|
|
483
|
+
@player_state.notify_step(step) if step.abs <= 24
|
|
484
|
+
end
|
|
485
|
+
@last_floor_height = new_floor
|
|
486
|
+
|
|
487
|
+
view_bob = @player_state.view_bob_offset
|
|
488
|
+
@renderer.set_z(new_floor + @player_state.viewheight + view_bob)
|
|
489
|
+
else
|
|
490
|
+
@renderer.set_z(new_floor + 41)
|
|
491
|
+
end
|
|
282
492
|
end
|
|
283
493
|
|
|
284
|
-
def
|
|
285
|
-
# Check if
|
|
286
|
-
sector = @map.sector_at(
|
|
494
|
+
def valid_move?(old_x, old_y, new_x, new_y)
|
|
495
|
+
# Check if destination is inside a valid sector
|
|
496
|
+
sector = @map.sector_at(new_x, new_y)
|
|
287
497
|
return false unless sector
|
|
288
498
|
|
|
289
|
-
# Check floor height - can't
|
|
499
|
+
# Check floor height - can't step up too high
|
|
290
500
|
floor_height = sector.floor_height
|
|
291
501
|
return false if floor_height > @renderer.player_z + 24 # Max step height
|
|
292
502
|
|
|
293
|
-
# Check against blocking linedefs
|
|
503
|
+
# Check against blocking linedefs: both circle intersection and path crossing
|
|
294
504
|
@map.linedefs.each do |linedef|
|
|
295
|
-
|
|
296
|
-
|
|
505
|
+
if linedef_blocks?(linedef, new_x, new_y)
|
|
506
|
+
return false
|
|
507
|
+
end
|
|
508
|
+
if crosses_blocking_linedef?(old_x, old_y, new_x, new_y, linedef)
|
|
509
|
+
return false
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# Check against solid things (monsters, barrels, pillars, etc.)
|
|
514
|
+
combined_radius = PLAYER_RADIUS
|
|
515
|
+
picked = @item_pickup&.picked_up
|
|
516
|
+
@map.things.each_with_index do |thing, idx|
|
|
517
|
+
next if picked && picked[idx]
|
|
518
|
+
next if @combat && @combat.dead?(idx)
|
|
519
|
+
thing_radius = SOLID_THING_RADIUS[thing.type]
|
|
520
|
+
next unless thing_radius
|
|
521
|
+
|
|
522
|
+
dx = new_x - thing.x
|
|
523
|
+
dy = new_y - thing.y
|
|
524
|
+
min_dist = combined_radius + thing_radius
|
|
525
|
+
if dx * dx + dy * dy < min_dist * min_dist
|
|
526
|
+
return false
|
|
527
|
+
end
|
|
297
528
|
end
|
|
298
529
|
|
|
299
530
|
true
|
|
300
531
|
end
|
|
301
532
|
|
|
533
|
+
# Check if movement from (x1,y1) to (x2,y2) crosses a blocking linedef
|
|
534
|
+
def crosses_blocking_linedef?(x1, y1, x2, y2, linedef)
|
|
535
|
+
v1 = @map.vertices[linedef.v1]
|
|
536
|
+
v2 = @map.vertices[linedef.v2]
|
|
537
|
+
|
|
538
|
+
# One-sided linedef always blocks crossing
|
|
539
|
+
if linedef.sidedef_left == 0xFFFF
|
|
540
|
+
return segments_intersect?(x1, y1, x2, y2, v1.x, v1.y, v2.x, v2.y)
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# BLOCKING flag blocks crossing even on two-sided linedefs
|
|
544
|
+
if (linedef.flags & 0x0001) != 0
|
|
545
|
+
return segments_intersect?(x1, y1, x2, y2, v1.x, v1.y, v2.x, v2.y)
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# Two-sided: check if impassable (high step OR low ceiling)
|
|
549
|
+
front_side = @map.sidedefs[linedef.sidedef_right]
|
|
550
|
+
back_side = @map.sidedefs[linedef.sidedef_left]
|
|
551
|
+
front_sector = @map.sectors[front_side.sector]
|
|
552
|
+
back_sector = @map.sectors[back_side.sector]
|
|
553
|
+
|
|
554
|
+
step = (back_sector.floor_height - front_sector.floor_height).abs
|
|
555
|
+
min_ceiling = [front_sector.ceiling_height, back_sector.ceiling_height].min
|
|
556
|
+
max_floor = [front_sector.floor_height, back_sector.floor_height].max
|
|
557
|
+
|
|
558
|
+
# Passable if step is small AND enough headroom
|
|
559
|
+
return false if step <= 24 && (min_ceiling - max_floor) >= 56
|
|
560
|
+
|
|
561
|
+
segments_intersect?(x1, y1, x2, y2, v1.x, v1.y, v2.x, v2.y)
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
# Test if line segment (ax1,ay1)-(ax2,ay2) intersects (bx1,by1)-(bx2,by2)
|
|
565
|
+
def segments_intersect?(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2)
|
|
566
|
+
d1x = ax2 - ax1
|
|
567
|
+
d1y = ay2 - ay1
|
|
568
|
+
d2x = bx2 - bx1
|
|
569
|
+
d2y = by2 - by1
|
|
570
|
+
|
|
571
|
+
denom = d1x * d2y - d1y * d2x
|
|
572
|
+
return false if denom.abs < 0.001 # Parallel
|
|
573
|
+
|
|
574
|
+
dx = bx1 - ax1
|
|
575
|
+
dy = by1 - ay1
|
|
576
|
+
|
|
577
|
+
t = (dx * d2y - dy * d2x).to_f / denom
|
|
578
|
+
u = (dx * d1y - dy * d1x).to_f / denom
|
|
579
|
+
|
|
580
|
+
t > 0.0 && t < 1.0 && u >= 0.0 && u <= 1.0
|
|
581
|
+
end
|
|
582
|
+
|
|
302
583
|
def linedef_blocks?(linedef, x, y)
|
|
303
584
|
v1 = @map.vertices[linedef.v1]
|
|
304
585
|
v2 = @map.vertices[linedef.v2]
|
|
@@ -309,23 +590,22 @@ module Doom
|
|
|
309
590
|
# One-sided linedef (wall) always blocks
|
|
310
591
|
return true if linedef.sidedef_left == 0xFFFF
|
|
311
592
|
|
|
312
|
-
#
|
|
593
|
+
# BLOCKING flag (0x0001) blocks even on two-sided linedefs (e.g., windows)
|
|
594
|
+
return true if (linedef.flags & 0x0001) != 0
|
|
595
|
+
|
|
596
|
+
# Two-sided: check if impassable (high step OR low ceiling)
|
|
313
597
|
front_side = @map.sidedefs[linedef.sidedef_right]
|
|
314
598
|
back_side = @map.sidedefs[linedef.sidedef_left]
|
|
315
599
|
|
|
316
600
|
front_sector = @map.sectors[front_side.sector]
|
|
317
601
|
back_sector = @map.sectors[back_side.sector]
|
|
318
602
|
|
|
319
|
-
|
|
320
|
-
step = back_sector.floor_height - front_sector.floor_height
|
|
321
|
-
return true if step.abs > 24
|
|
322
|
-
|
|
323
|
-
# Check ceiling clearance
|
|
603
|
+
step = (back_sector.floor_height - front_sector.floor_height).abs
|
|
324
604
|
min_ceiling = [front_sector.ceiling_height, back_sector.ceiling_height].min
|
|
325
605
|
max_floor = [front_sector.floor_height, back_sector.floor_height].max
|
|
326
|
-
return true if min_ceiling - max_floor < 56 # Player height
|
|
327
606
|
|
|
328
|
-
|
|
607
|
+
# Block if step too high OR not enough headroom
|
|
608
|
+
step > 24 || (min_ceiling - max_floor) < 56
|
|
329
609
|
end
|
|
330
610
|
|
|
331
611
|
def line_circle_intersect?(x1, y1, x2, y2, cx, cy, radius)
|
|
@@ -376,16 +656,60 @@ module Doom
|
|
|
376
656
|
end
|
|
377
657
|
|
|
378
658
|
def draw
|
|
379
|
-
|
|
380
|
-
|
|
659
|
+
if @show_map
|
|
660
|
+
draw_automap
|
|
661
|
+
else
|
|
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
|
|
666
|
+
|
|
667
|
+
@screen_image = Gosu::Image.from_blob(
|
|
668
|
+
Render::SCREEN_WIDTH,
|
|
669
|
+
Render::SCREEN_HEIGHT,
|
|
670
|
+
rgba
|
|
671
|
+
)
|
|
381
672
|
|
|
382
|
-
|
|
383
|
-
Render::SCREEN_WIDTH,
|
|
384
|
-
Render::SCREEN_HEIGHT,
|
|
385
|
-
rgba
|
|
386
|
-
)
|
|
673
|
+
@screen_image.draw(0, 0, 0, SCALE, SCALE)
|
|
387
674
|
|
|
388
|
-
|
|
675
|
+
draw_debug_overlay if @show_debug
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
|
|
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
|
+
|
|
690
|
+
sector = @map.sector_at(@renderer.player_x, @renderer.player_y)
|
|
691
|
+
return unless sector
|
|
692
|
+
|
|
693
|
+
# Find sector index
|
|
694
|
+
sector_idx = @map.sectors.index(sector)
|
|
695
|
+
|
|
696
|
+
lines = [
|
|
697
|
+
"FPS: #{@fps_display}",
|
|
698
|
+
"Sector #{sector_idx}",
|
|
699
|
+
"Floor: #{sector.floor_height} (#{sector.floor_texture})",
|
|
700
|
+
"Ceil: #{sector.ceiling_height} (#{sector.ceiling_texture})",
|
|
701
|
+
"Light: #{sector.light_level}",
|
|
702
|
+
"Pos: #{@renderer.player_x.round}, #{@renderer.player_y.round}",
|
|
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)",
|
|
705
|
+
]
|
|
706
|
+
|
|
707
|
+
y = 4
|
|
708
|
+
lines.each do |line|
|
|
709
|
+
@debug_font.draw_text(line, 6, y + 1, 1, 1, 1, Gosu::Color::BLACK)
|
|
710
|
+
@debug_font.draw_text(line, 5, y, 1, 1, 1, Gosu::Color::WHITE)
|
|
711
|
+
y += 18
|
|
712
|
+
end
|
|
389
713
|
end
|
|
390
714
|
|
|
391
715
|
def button_down(id)
|
|
@@ -403,9 +727,287 @@ module Doom
|
|
|
403
727
|
@mouse_captured = true
|
|
404
728
|
@last_mouse_x = mouse_x
|
|
405
729
|
end
|
|
730
|
+
when Gosu::KB_Z
|
|
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
|
|
743
|
+
when Gosu::KB_M
|
|
744
|
+
@show_map = !@show_map
|
|
745
|
+
when Gosu::KB_F12
|
|
746
|
+
capture_debug_snapshot
|
|
747
|
+
end
|
|
748
|
+
end
|
|
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
|
+
|
|
814
|
+
def needs_cursor?
|
|
815
|
+
!@mouse_captured
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
# --- Debug Snapshot ---
|
|
819
|
+
|
|
820
|
+
def capture_debug_snapshot
|
|
821
|
+
dir = File.join(File.expand_path('../..', __dir__), '..', 'screenshots')
|
|
822
|
+
FileUtils.mkdir_p(dir)
|
|
823
|
+
|
|
824
|
+
ts = Time.now.strftime('%Y%m%d_%H%M%S_%L')
|
|
825
|
+
prefix = File.join(dir, ts)
|
|
826
|
+
|
|
827
|
+
# Save framebuffer as PNG
|
|
828
|
+
require 'chunky_png' unless defined?(ChunkyPNG)
|
|
829
|
+
w = Render::SCREEN_WIDTH
|
|
830
|
+
h = Render::SCREEN_HEIGHT
|
|
831
|
+
img = ChunkyPNG::Image.new(w, h)
|
|
832
|
+
fb = @renderer.framebuffer
|
|
833
|
+
colors = @palette.colors
|
|
834
|
+
h.times do |y|
|
|
835
|
+
row = y * w
|
|
836
|
+
w.times do |x|
|
|
837
|
+
r, g, b = colors[fb[row + x]]
|
|
838
|
+
img[x, y] = ChunkyPNG::Color.rgb(r, g, b)
|
|
839
|
+
end
|
|
840
|
+
end
|
|
841
|
+
img.save("#{prefix}.png")
|
|
842
|
+
|
|
843
|
+
# Save player state and sector info
|
|
844
|
+
sector = @map.sector_at(@renderer.player_x, @renderer.player_y)
|
|
845
|
+
sector_idx = sector ? @map.sectors.index(sector) : nil
|
|
846
|
+
angle_deg = Math.atan2(@renderer.sin_angle, @renderer.cos_angle) * 180.0 / Math::PI
|
|
847
|
+
|
|
848
|
+
# Sprite diagnostics
|
|
849
|
+
sprites_info = @renderer.sprite_diagnostics
|
|
850
|
+
nearby = sprites_info.select { |s| s[:dist] && s[:dist] < 1500 }
|
|
851
|
+
.sort_by { |s| s[:dist] }
|
|
852
|
+
|
|
853
|
+
sprite_lines = nearby.map do |s|
|
|
854
|
+
" #{s[:prefix]} type=#{s[:type]} pos=(#{s[:x]},#{s[:y]}) dist=#{s[:dist]} " \
|
|
855
|
+
"screen_x=#{s[:screen_x]} scale=#{s[:sprite_scale]} " \
|
|
856
|
+
"range=#{s[:screen_range]} status=#{s[:status]} " \
|
|
857
|
+
"clip_segs=#{s[:clipping_segs]}" \
|
|
858
|
+
"#{s[:clipping_detail]&.any? ? "\n clips: #{s[:clipping_detail].map { |c| "ds[#{c[:x1]}..#{c[:x2]}] scale=#{c[:scale]} sil=#{c[:sil]}" }.join(', ')}" : ''}"
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
File.write("#{prefix}.txt", <<~INFO)
|
|
862
|
+
pos: #{@renderer.player_x.round(1)}, #{@renderer.player_y.round(1)}, #{@renderer.player_z.round(1)}
|
|
863
|
+
angle: #{angle_deg.round(1)}
|
|
864
|
+
sector: #{sector_idx}
|
|
865
|
+
floor: #{sector&.floor_height} (#{sector&.floor_texture})
|
|
866
|
+
ceil: #{sector&.ceiling_height} (#{sector&.ceiling_texture})
|
|
867
|
+
light: #{sector&.light_level}
|
|
868
|
+
|
|
869
|
+
nearby sprites (#{nearby.size}):
|
|
870
|
+
#{sprite_lines.join("\n")}
|
|
871
|
+
INFO
|
|
872
|
+
|
|
873
|
+
puts "Snapshot saved: #{prefix}.png + .txt"
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
# --- Automap ---
|
|
877
|
+
|
|
878
|
+
MAP_MARGIN = 20
|
|
879
|
+
|
|
880
|
+
def build_sector_colors
|
|
881
|
+
# Generate distinct colors for each sector using golden ratio hue spacing
|
|
882
|
+
num_sectors = @map.sectors.size
|
|
883
|
+
colors = Array.new(num_sectors)
|
|
884
|
+
phi = (1 + Math.sqrt(5)) / 2.0
|
|
885
|
+
|
|
886
|
+
num_sectors.times do |i|
|
|
887
|
+
hue = (i * phi * 360) % 360
|
|
888
|
+
colors[i] = hsv_to_gosu(hue, 0.6, 0.85)
|
|
889
|
+
end
|
|
890
|
+
colors
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
def hsv_to_gosu(h, s, v)
|
|
894
|
+
c = v * s
|
|
895
|
+
x = c * (1 - ((h / 60.0) % 2 - 1).abs)
|
|
896
|
+
m = v - c
|
|
897
|
+
|
|
898
|
+
r, g, b = case (h / 60).to_i % 6
|
|
899
|
+
when 0 then [c, x, 0]
|
|
900
|
+
when 1 then [x, c, 0]
|
|
901
|
+
when 2 then [0, c, x]
|
|
902
|
+
when 3 then [0, x, c]
|
|
903
|
+
when 4 then [x, 0, c]
|
|
904
|
+
when 5 then [c, 0, x]
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
Gosu::Color.new(255, ((r + m) * 255).to_i, ((g + m) * 255).to_i, ((b + m) * 255).to_i)
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
def draw_automap
|
|
911
|
+
# Black background
|
|
912
|
+
Gosu.draw_rect(0, 0, width, height, Gosu::Color::BLACK, 0)
|
|
913
|
+
|
|
914
|
+
# Compute map bounds
|
|
915
|
+
verts = @map.vertices
|
|
916
|
+
min_x = min_y = Float::INFINITY
|
|
917
|
+
max_x = max_y = -Float::INFINITY
|
|
918
|
+
verts.each do |v|
|
|
919
|
+
min_x = v.x if v.x < min_x
|
|
920
|
+
max_x = v.x if v.x > max_x
|
|
921
|
+
min_y = v.y if v.y < min_y
|
|
922
|
+
max_y = v.y if v.y > max_y
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
map_w = max_x - min_x
|
|
926
|
+
map_h = max_y - min_y
|
|
927
|
+
return if map_w == 0 || map_h == 0
|
|
928
|
+
|
|
929
|
+
# Scale to fit screen with margin
|
|
930
|
+
draw_w = width - MAP_MARGIN * 2
|
|
931
|
+
draw_h = height - MAP_MARGIN * 2
|
|
932
|
+
scale = [draw_w.to_f / map_w, draw_h.to_f / map_h].min
|
|
933
|
+
|
|
934
|
+
# Center the map
|
|
935
|
+
offset_x = MAP_MARGIN + (draw_w - map_w * scale) / 2.0
|
|
936
|
+
offset_y = MAP_MARGIN + (draw_h - map_h * scale) / 2.0
|
|
937
|
+
|
|
938
|
+
# World to screen coordinate transform (Y flipped: world Y+ is up, screen Y+ is down)
|
|
939
|
+
to_sx = ->(wx) { offset_x + (wx - min_x) * scale }
|
|
940
|
+
to_sy = ->(wy) { offset_y + (max_y - wy) * scale }
|
|
941
|
+
|
|
942
|
+
# Draw linedefs colored by front sector
|
|
943
|
+
two_sided_color = Gosu::Color.new(100, 80, 80, 80)
|
|
944
|
+
|
|
945
|
+
@map.linedefs.each do |linedef|
|
|
946
|
+
v1 = verts[linedef.v1]
|
|
947
|
+
v2 = verts[linedef.v2]
|
|
948
|
+
sx1 = to_sx.call(v1.x)
|
|
949
|
+
sy1 = to_sy.call(v1.y)
|
|
950
|
+
sx2 = to_sx.call(v2.x)
|
|
951
|
+
sy2 = to_sy.call(v2.y)
|
|
952
|
+
|
|
953
|
+
if linedef.two_sided?
|
|
954
|
+
# Two-sided: dim line, colored by front sector
|
|
955
|
+
front_sd = @map.sidedefs[linedef.sidedef_right]
|
|
956
|
+
color = @sector_colors[front_sd.sector]
|
|
957
|
+
dim = Gosu::Color.new(100, color.red, color.green, color.blue)
|
|
958
|
+
Gosu.draw_line(sx1, sy1, dim, sx2, sy2, dim, 1)
|
|
959
|
+
else
|
|
960
|
+
# One-sided: solid wall, bright sector color
|
|
961
|
+
front_sd = @map.sidedefs[linedef.sidedef_right]
|
|
962
|
+
color = @sector_colors[front_sd.sector]
|
|
963
|
+
Gosu.draw_line(sx1, sy1, color, sx2, sy2, color, 1)
|
|
964
|
+
end
|
|
406
965
|
end
|
|
966
|
+
|
|
967
|
+
# Draw player
|
|
968
|
+
px = to_sx.call(@renderer.player_x)
|
|
969
|
+
py = to_sy.call(@renderer.player_y)
|
|
970
|
+
|
|
971
|
+
cos_a = @renderer.cos_angle
|
|
972
|
+
sin_a = @renderer.sin_angle
|
|
973
|
+
|
|
974
|
+
# FOV cone
|
|
975
|
+
fov_len = 40.0
|
|
976
|
+
half_fov = Math::PI / 4.0 # 45 deg half = 90 deg total
|
|
977
|
+
|
|
978
|
+
# Cone edges (in world space, Y+ is up; on screen Y is flipped via to_sy)
|
|
979
|
+
left_dx = Math.cos(half_fov) * cos_a - Math.sin(half_fov) * sin_a
|
|
980
|
+
left_dy = Math.cos(half_fov) * sin_a + Math.sin(half_fov) * cos_a
|
|
981
|
+
right_dx = Math.cos(-half_fov) * cos_a - Math.sin(-half_fov) * sin_a
|
|
982
|
+
right_dy = Math.cos(-half_fov) * sin_a + Math.sin(-half_fov) * cos_a
|
|
983
|
+
|
|
984
|
+
# Screen positions for cone tips
|
|
985
|
+
lx = px + left_dx * fov_len
|
|
986
|
+
ly = py - left_dy * fov_len # negate because screen Y is flipped
|
|
987
|
+
rx = px + right_dx * fov_len
|
|
988
|
+
ry = py - right_dy * fov_len
|
|
989
|
+
|
|
990
|
+
cone_color = Gosu::Color.new(60, 0, 255, 0)
|
|
991
|
+
Gosu.draw_triangle(px, py, cone_color, lx, ly, cone_color, rx, ry, cone_color, 2)
|
|
992
|
+
|
|
993
|
+
# Cone edge lines
|
|
994
|
+
edge_color = Gosu::Color.new(180, 0, 255, 0)
|
|
995
|
+
Gosu.draw_line(px, py, edge_color, lx, ly, edge_color, 3)
|
|
996
|
+
Gosu.draw_line(px, py, edge_color, rx, ry, edge_color, 3)
|
|
997
|
+
|
|
998
|
+
# Player dot
|
|
999
|
+
dot_size = 4
|
|
1000
|
+
Gosu.draw_rect(px - dot_size, py - dot_size, dot_size * 2, dot_size * 2, Gosu::Color::GREEN, 3)
|
|
1001
|
+
|
|
1002
|
+
# Direction line
|
|
1003
|
+
dir_len = 12.0
|
|
1004
|
+
dx = px + cos_a * dir_len
|
|
1005
|
+
dy = py - sin_a * dir_len
|
|
1006
|
+
Gosu.draw_line(px, py, Gosu::Color::WHITE, dx, dy, Gosu::Color::WHITE, 3)
|
|
407
1007
|
end
|
|
408
1008
|
|
|
1009
|
+
# --- End Automap ---
|
|
1010
|
+
|
|
409
1011
|
def needs_cursor?
|
|
410
1012
|
!@mouse_captured
|
|
411
1013
|
end
|