doom 0.3.0 → 0.4.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/player_state.rb +221 -0
- data/lib/doom/game/sector_actions.rb +162 -0
- data/lib/doom/platform/gosu_window.rb +179 -2
- data/lib/doom/render/renderer.rb +140 -86
- data/lib/doom/render/status_bar.rb +166 -0
- data/lib/doom/render/weapon_renderer.rb +102 -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 +189 -0
- data/lib/doom/wad/texture.rb +0 -10
- data/lib/doom/wad_downloader.rb +2 -2
- data/lib/doom.rb +15 -1
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cb48bcb7558c2e4f4ab959b16dbd857d2a1b6a3b9a9cc76c19a08ec91bdeb93e
|
|
4
|
+
data.tar.gz: 610f42cf954c60048a0c4030d96a8e8ceb2872aba90e90a141b2179329867c05
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1ff09fd05c29a5c49b7d41285424d58b8b46dbf995e1f18106d571f18866762e8b20ecb47d472dc2cb2be8f2fc8cdea5eb0b3a2dedae55d5b1881bbcb796088b
|
|
7
|
+
data.tar.gz: 41d4fcb793f3b25fb57f2d50c02f5eee1a56cb40a40baf2a9f7e0831634f43aa4777abf80968130c44593055dbaea23bd3257e7d4b509a068af2c92e1e16698d
|
data/README.md
CHANGED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Game
|
|
5
|
+
# Tracks player state for HUD display and weapon rendering
|
|
6
|
+
class PlayerState
|
|
7
|
+
# Weapons
|
|
8
|
+
WEAPON_FIST = 0
|
|
9
|
+
WEAPON_PISTOL = 1
|
|
10
|
+
WEAPON_SHOTGUN = 2
|
|
11
|
+
WEAPON_CHAINGUN = 3
|
|
12
|
+
WEAPON_ROCKET = 4
|
|
13
|
+
WEAPON_PLASMA = 5
|
|
14
|
+
WEAPON_BFG = 6
|
|
15
|
+
WEAPON_CHAINSAW = 7
|
|
16
|
+
|
|
17
|
+
# Weapon symbols for graphics lookup
|
|
18
|
+
WEAPON_NAMES = {
|
|
19
|
+
WEAPON_FIST => :fist,
|
|
20
|
+
WEAPON_PISTOL => :pistol,
|
|
21
|
+
WEAPON_SHOTGUN => :shotgun,
|
|
22
|
+
WEAPON_CHAINGUN => :chaingun,
|
|
23
|
+
WEAPON_ROCKET => :rocket,
|
|
24
|
+
WEAPON_PLASMA => :plasma,
|
|
25
|
+
WEAPON_BFG => :bfg,
|
|
26
|
+
WEAPON_CHAINSAW => :chainsaw
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
# Attack durations (in frames at 35fps)
|
|
30
|
+
ATTACK_DURATIONS = {
|
|
31
|
+
WEAPON_FIST => 12,
|
|
32
|
+
WEAPON_PISTOL => 8,
|
|
33
|
+
WEAPON_SHOTGUN => 20,
|
|
34
|
+
WEAPON_CHAINGUN => 4,
|
|
35
|
+
WEAPON_ROCKET => 16,
|
|
36
|
+
WEAPON_PLASMA => 6,
|
|
37
|
+
WEAPON_BFG => 40,
|
|
38
|
+
WEAPON_CHAINSAW => 4
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
attr_accessor :health, :armor, :max_health, :max_armor
|
|
42
|
+
attr_accessor :ammo_bullets, :ammo_shells, :ammo_rockets, :ammo_cells
|
|
43
|
+
attr_accessor :max_bullets, :max_shells, :max_rockets, :max_cells
|
|
44
|
+
attr_accessor :weapon, :has_weapons
|
|
45
|
+
attr_accessor :keys
|
|
46
|
+
attr_accessor :attacking, :attack_frame, :attack_tics
|
|
47
|
+
attr_accessor :bob_angle, :bob_amount
|
|
48
|
+
attr_accessor :is_moving
|
|
49
|
+
|
|
50
|
+
def initialize
|
|
51
|
+
reset
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def reset
|
|
55
|
+
@health = 100
|
|
56
|
+
@armor = 0
|
|
57
|
+
@max_health = 100
|
|
58
|
+
@max_armor = 200
|
|
59
|
+
|
|
60
|
+
# Ammo
|
|
61
|
+
@ammo_bullets = 50
|
|
62
|
+
@ammo_shells = 0
|
|
63
|
+
@ammo_rockets = 0
|
|
64
|
+
@ammo_cells = 0
|
|
65
|
+
|
|
66
|
+
@max_bullets = 200
|
|
67
|
+
@max_shells = 50
|
|
68
|
+
@max_rockets = 50
|
|
69
|
+
@max_cells = 300
|
|
70
|
+
|
|
71
|
+
# Start with fist and pistol
|
|
72
|
+
@weapon = WEAPON_PISTOL
|
|
73
|
+
@has_weapons = [true, true, false, false, false, false, false, false]
|
|
74
|
+
|
|
75
|
+
# No keys
|
|
76
|
+
@keys = {
|
|
77
|
+
blue_card: false,
|
|
78
|
+
yellow_card: false,
|
|
79
|
+
red_card: false,
|
|
80
|
+
blue_skull: false,
|
|
81
|
+
yellow_skull: false,
|
|
82
|
+
red_skull: false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Attack state
|
|
86
|
+
@attacking = false
|
|
87
|
+
@attack_frame = 0
|
|
88
|
+
@attack_tics = 0
|
|
89
|
+
|
|
90
|
+
# Weapon bob
|
|
91
|
+
@bob_angle = 0.0
|
|
92
|
+
@bob_amount = 0.0
|
|
93
|
+
@is_moving = false
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def weapon_name
|
|
97
|
+
WEAPON_NAMES[@weapon]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def current_ammo
|
|
101
|
+
case @weapon
|
|
102
|
+
when WEAPON_PISTOL, WEAPON_CHAINGUN
|
|
103
|
+
@ammo_bullets
|
|
104
|
+
when WEAPON_SHOTGUN
|
|
105
|
+
@ammo_shells
|
|
106
|
+
when WEAPON_ROCKET
|
|
107
|
+
@ammo_rockets
|
|
108
|
+
when WEAPON_PLASMA, WEAPON_BFG
|
|
109
|
+
@ammo_cells
|
|
110
|
+
else
|
|
111
|
+
nil # Fist/chainsaw don't use ammo
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def max_ammo_for_weapon
|
|
116
|
+
case @weapon
|
|
117
|
+
when WEAPON_PISTOL, WEAPON_CHAINGUN
|
|
118
|
+
@max_bullets
|
|
119
|
+
when WEAPON_SHOTGUN
|
|
120
|
+
@max_shells
|
|
121
|
+
when WEAPON_ROCKET
|
|
122
|
+
@max_rockets
|
|
123
|
+
when WEAPON_PLASMA, WEAPON_BFG
|
|
124
|
+
@max_cells
|
|
125
|
+
else
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def can_attack?
|
|
131
|
+
return true if @weapon == WEAPON_FIST || @weapon == WEAPON_CHAINSAW
|
|
132
|
+
|
|
133
|
+
ammo = current_ammo
|
|
134
|
+
ammo && ammo > 0
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def start_attack
|
|
138
|
+
return unless can_attack?
|
|
139
|
+
return if @attacking
|
|
140
|
+
|
|
141
|
+
@attacking = true
|
|
142
|
+
@attack_frame = 0
|
|
143
|
+
@attack_tics = 0
|
|
144
|
+
|
|
145
|
+
# Consume ammo
|
|
146
|
+
case @weapon
|
|
147
|
+
when WEAPON_PISTOL
|
|
148
|
+
@ammo_bullets -= 1 if @ammo_bullets > 0
|
|
149
|
+
when WEAPON_SHOTGUN
|
|
150
|
+
@ammo_shells -= 1 if @ammo_shells > 0
|
|
151
|
+
when WEAPON_CHAINGUN
|
|
152
|
+
@ammo_bullets -= 1 if @ammo_bullets > 0
|
|
153
|
+
when WEAPON_ROCKET
|
|
154
|
+
@ammo_rockets -= 1 if @ammo_rockets > 0
|
|
155
|
+
when WEAPON_PLASMA
|
|
156
|
+
@ammo_cells -= 1 if @ammo_cells > 0
|
|
157
|
+
when WEAPON_BFG
|
|
158
|
+
@ammo_cells -= 40 if @ammo_cells >= 40
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def update_attack
|
|
163
|
+
return unless @attacking
|
|
164
|
+
|
|
165
|
+
@attack_tics += 1
|
|
166
|
+
|
|
167
|
+
# Calculate which frame we're on based on tics
|
|
168
|
+
duration = ATTACK_DURATIONS[@weapon] || 8
|
|
169
|
+
frame_count = @weapon == WEAPON_FIST ? 3 : 4
|
|
170
|
+
|
|
171
|
+
tics_per_frame = duration / frame_count
|
|
172
|
+
@attack_frame = (@attack_tics / tics_per_frame).to_i
|
|
173
|
+
|
|
174
|
+
# Attack finished?
|
|
175
|
+
if @attack_tics >= duration
|
|
176
|
+
@attacking = false
|
|
177
|
+
@attack_frame = 0
|
|
178
|
+
@attack_tics = 0
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def update_bob(delta_time)
|
|
183
|
+
if @is_moving
|
|
184
|
+
# Increase bob while moving
|
|
185
|
+
@bob_angle += delta_time * 10.0
|
|
186
|
+
@bob_amount = [@bob_amount + delta_time * 16.0, 6.0].min
|
|
187
|
+
else
|
|
188
|
+
# Decay bob when stopped
|
|
189
|
+
@bob_amount = [@bob_amount - delta_time * 12.0, 0.0].max
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def weapon_bob_x
|
|
194
|
+
Math.cos(@bob_angle) * @bob_amount
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def weapon_bob_y
|
|
198
|
+
Math.sin(@bob_angle * 2) * @bob_amount * 0.5
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def health_level
|
|
202
|
+
# 0 = dying, 4 = full health
|
|
203
|
+
case @health
|
|
204
|
+
when 80..200 then 4
|
|
205
|
+
when 60..79 then 3
|
|
206
|
+
when 40..59 then 2
|
|
207
|
+
when 20..39 then 1
|
|
208
|
+
else 0
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def switch_weapon(weapon_num)
|
|
213
|
+
return unless weapon_num >= 0 && weapon_num < 8
|
|
214
|
+
return unless @has_weapons[weapon_num]
|
|
215
|
+
return if @attacking
|
|
216
|
+
|
|
217
|
+
@weapon = weapon_num
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Game
|
|
5
|
+
# Manages animated sector actions (doors, lifts, etc.)
|
|
6
|
+
class SectorActions
|
|
7
|
+
# Door states
|
|
8
|
+
DOOR_CLOSED = 0
|
|
9
|
+
DOOR_OPENING = 1
|
|
10
|
+
DOOR_OPEN = 2
|
|
11
|
+
DOOR_CLOSING = 3
|
|
12
|
+
|
|
13
|
+
# Door speeds (units per tic, 35 tics/sec)
|
|
14
|
+
DOOR_SPEED = 2
|
|
15
|
+
DOOR_WAIT = 150 # Tics to wait when open (~4 seconds)
|
|
16
|
+
PLAYER_HEIGHT = 56 # Player height for door collision
|
|
17
|
+
|
|
18
|
+
def initialize(map)
|
|
19
|
+
@map = map
|
|
20
|
+
@active_doors = {} # sector_index => door_state
|
|
21
|
+
@player_x = 0
|
|
22
|
+
@player_y = 0
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def update_player_position(x, y)
|
|
26
|
+
@player_x = x
|
|
27
|
+
@player_y = y
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def update
|
|
31
|
+
update_doors
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Try to use a linedef (called when player presses use key)
|
|
35
|
+
def use_linedef(linedef, linedef_idx)
|
|
36
|
+
return false if linedef.special == 0
|
|
37
|
+
|
|
38
|
+
case linedef.special
|
|
39
|
+
when 1 # DR Door Open Wait Close
|
|
40
|
+
activate_door(linedef)
|
|
41
|
+
true
|
|
42
|
+
when 31 # D1 Door Open Stay
|
|
43
|
+
activate_door(linedef, stay_open: true)
|
|
44
|
+
true
|
|
45
|
+
when 26 # DR Blue Door
|
|
46
|
+
activate_door(linedef, key: :blue_card)
|
|
47
|
+
true
|
|
48
|
+
when 27 # DR Yellow Door
|
|
49
|
+
activate_door(linedef, key: :yellow_card)
|
|
50
|
+
true
|
|
51
|
+
when 28 # DR Red Door
|
|
52
|
+
activate_door(linedef, key: :red_card)
|
|
53
|
+
true
|
|
54
|
+
else
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def activate_door(linedef, stay_open: false, key: nil)
|
|
62
|
+
# Find the sector on the back side of the linedef
|
|
63
|
+
return unless linedef.two_sided?
|
|
64
|
+
|
|
65
|
+
back_sidedef_idx = linedef.sidedef_left
|
|
66
|
+
return if back_sidedef_idx == 0xFFFF || back_sidedef_idx < 0
|
|
67
|
+
|
|
68
|
+
back_sidedef = @map.sidedefs[back_sidedef_idx]
|
|
69
|
+
sector_idx = back_sidedef.sector
|
|
70
|
+
sector = @map.sectors[sector_idx]
|
|
71
|
+
return unless sector
|
|
72
|
+
|
|
73
|
+
# Check if door is already active
|
|
74
|
+
if @active_doors[sector_idx]
|
|
75
|
+
door = @active_doors[sector_idx]
|
|
76
|
+
# If closing, reverse direction
|
|
77
|
+
if door[:state] == DOOR_CLOSING
|
|
78
|
+
door[:state] = DOOR_OPENING
|
|
79
|
+
end
|
|
80
|
+
return
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Calculate target height (find lowest adjacent ceiling)
|
|
84
|
+
target_height = find_lowest_ceiling_around(sector_idx) - 4
|
|
85
|
+
|
|
86
|
+
# Start the door
|
|
87
|
+
@active_doors[sector_idx] = {
|
|
88
|
+
sector: sector,
|
|
89
|
+
state: DOOR_OPENING,
|
|
90
|
+
target_height: target_height,
|
|
91
|
+
original_height: sector.ceiling_height,
|
|
92
|
+
wait_tics: 0,
|
|
93
|
+
stay_open: stay_open
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def update_doors
|
|
98
|
+
@active_doors.each do |sector_idx, door|
|
|
99
|
+
case door[:state]
|
|
100
|
+
when DOOR_OPENING
|
|
101
|
+
door[:sector].ceiling_height += DOOR_SPEED
|
|
102
|
+
if door[:sector].ceiling_height >= door[:target_height]
|
|
103
|
+
door[:sector].ceiling_height = door[:target_height]
|
|
104
|
+
if door[:stay_open]
|
|
105
|
+
@active_doors.delete(sector_idx)
|
|
106
|
+
else
|
|
107
|
+
door[:state] = DOOR_OPEN
|
|
108
|
+
door[:wait_tics] = DOOR_WAIT
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
when DOOR_OPEN
|
|
113
|
+
door[:wait_tics] -= 1
|
|
114
|
+
if door[:wait_tics] <= 0
|
|
115
|
+
door[:state] = DOOR_CLOSING
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
when DOOR_CLOSING
|
|
119
|
+
# Check if player is in the door sector
|
|
120
|
+
player_sector = @map.sector_at(@player_x, @player_y)
|
|
121
|
+
if player_sector == door[:sector]
|
|
122
|
+
# Player is in door - reopen it
|
|
123
|
+
door[:state] = DOOR_OPENING
|
|
124
|
+
next
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
door[:sector].ceiling_height -= DOOR_SPEED
|
|
128
|
+
if door[:sector].ceiling_height <= door[:original_height]
|
|
129
|
+
door[:sector].ceiling_height = door[:original_height]
|
|
130
|
+
@active_doors.delete(sector_idx)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def find_lowest_ceiling_around(sector_idx)
|
|
137
|
+
lowest = Float::INFINITY
|
|
138
|
+
|
|
139
|
+
@map.linedefs.each do |linedef|
|
|
140
|
+
next unless linedef.two_sided?
|
|
141
|
+
|
|
142
|
+
# Check if this linedef touches our sector
|
|
143
|
+
right_sidedef = @map.sidedefs[linedef.sidedef_right]
|
|
144
|
+
left_sidedef = @map.sidedefs[linedef.sidedef_left] if linedef.sidedef_left != 0xFFFF
|
|
145
|
+
|
|
146
|
+
adjacent_sector = nil
|
|
147
|
+
if right_sidedef&.sector == sector_idx && left_sidedef
|
|
148
|
+
adjacent_sector = @map.sectors[left_sidedef.sector]
|
|
149
|
+
elsif left_sidedef&.sector == sector_idx
|
|
150
|
+
adjacent_sector = @map.sectors[right_sidedef.sector]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
if adjacent_sector
|
|
154
|
+
lowest = [lowest, adjacent_sector.ceiling_height].min
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
lowest == Float::INFINITY ? 128 : lowest
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -13,24 +13,62 @@ module Doom
|
|
|
13
13
|
MOUSE_SENSITIVITY = 0.15 # Mouse look sensitivity
|
|
14
14
|
PLAYER_RADIUS = 16.0 # Collision radius
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
USE_DISTANCE = 64.0 # Max distance to use a linedef
|
|
17
|
+
|
|
18
|
+
def initialize(renderer, palette, map, player_state = nil, status_bar = nil, weapon_renderer = nil, sector_actions = nil)
|
|
17
19
|
super(Render::SCREEN_WIDTH * SCALE, Render::SCREEN_HEIGHT * SCALE, false)
|
|
18
20
|
self.caption = 'Doom Ruby'
|
|
19
21
|
|
|
20
22
|
@renderer = renderer
|
|
21
23
|
@palette = palette
|
|
22
24
|
@map = map
|
|
25
|
+
@player_state = player_state
|
|
26
|
+
@status_bar = status_bar
|
|
27
|
+
@weapon_renderer = weapon_renderer
|
|
28
|
+
@sector_actions = sector_actions
|
|
23
29
|
@screen_image = nil
|
|
24
30
|
@mouse_captured = false
|
|
25
31
|
@last_mouse_x = nil
|
|
32
|
+
@last_update_time = Time.now
|
|
33
|
+
@use_pressed = false
|
|
26
34
|
|
|
27
35
|
# Pre-build palette lookup for speed
|
|
28
36
|
@palette_rgba = palette.colors.map { |r, g, b| [r, g, b, 255].pack('CCCC') }
|
|
29
37
|
end
|
|
30
38
|
|
|
31
39
|
def update
|
|
40
|
+
# Calculate delta time for smooth animations
|
|
41
|
+
now = Time.now
|
|
42
|
+
delta_time = now - @last_update_time
|
|
43
|
+
@last_update_time = now
|
|
44
|
+
|
|
32
45
|
handle_input
|
|
46
|
+
|
|
47
|
+
# Update player state
|
|
48
|
+
if @player_state
|
|
49
|
+
@player_state.update_attack
|
|
50
|
+
@player_state.update_bob(delta_time)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Update HUD animations
|
|
54
|
+
@status_bar&.update
|
|
55
|
+
|
|
56
|
+
# Update sector actions (doors, lifts, etc.)
|
|
57
|
+
if @sector_actions
|
|
58
|
+
@sector_actions.update_player_position(@renderer.player_x, @renderer.player_y)
|
|
59
|
+
@sector_actions.update
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Render the 3D world
|
|
33
63
|
@renderer.render_frame
|
|
64
|
+
|
|
65
|
+
# Render HUD on top
|
|
66
|
+
if @weapon_renderer
|
|
67
|
+
@weapon_renderer.render(@renderer.framebuffer)
|
|
68
|
+
end
|
|
69
|
+
if @status_bar
|
|
70
|
+
@status_bar.render(@renderer.framebuffer)
|
|
71
|
+
end
|
|
34
72
|
end
|
|
35
73
|
|
|
36
74
|
def handle_input
|
|
@@ -68,10 +106,149 @@ module Doom
|
|
|
68
106
|
move_y += @renderer.cos_angle * MOVE_SPEED
|
|
69
107
|
end
|
|
70
108
|
|
|
109
|
+
# Track if player is moving (for weapon bob)
|
|
110
|
+
is_moving = move_x != 0.0 || move_y != 0.0
|
|
111
|
+
@player_state.is_moving = is_moving if @player_state
|
|
112
|
+
|
|
71
113
|
# Apply movement with collision detection
|
|
72
|
-
if
|
|
114
|
+
if is_moving
|
|
73
115
|
try_move(move_x, move_y)
|
|
74
116
|
end
|
|
117
|
+
|
|
118
|
+
# Handle firing
|
|
119
|
+
if @player_state && @mouse_captured && Gosu.button_down?(Gosu::MS_LEFT)
|
|
120
|
+
@player_state.start_attack
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Handle weapon switching with number keys
|
|
124
|
+
handle_weapon_switch if @player_state
|
|
125
|
+
|
|
126
|
+
# Handle use key (spacebar or E)
|
|
127
|
+
handle_use_key if @sector_actions
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def handle_use_key
|
|
131
|
+
use_down = Gosu.button_down?(Gosu::KB_SPACE) || Gosu.button_down?(Gosu::KB_E)
|
|
132
|
+
|
|
133
|
+
if use_down && !@use_pressed
|
|
134
|
+
@use_pressed = true
|
|
135
|
+
try_use_linedef
|
|
136
|
+
elsif !use_down
|
|
137
|
+
@use_pressed = false
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def try_use_linedef
|
|
142
|
+
# Cast a ray forward to find a usable linedef
|
|
143
|
+
player_x = @renderer.player_x
|
|
144
|
+
player_y = @renderer.player_y
|
|
145
|
+
cos_angle = @renderer.cos_angle
|
|
146
|
+
sin_angle = @renderer.sin_angle
|
|
147
|
+
|
|
148
|
+
# Check point in front of player
|
|
149
|
+
use_x = player_x + cos_angle * USE_DISTANCE
|
|
150
|
+
use_y = player_y + sin_angle * USE_DISTANCE
|
|
151
|
+
|
|
152
|
+
# Find the closest linedef the player is facing
|
|
153
|
+
best_linedef = nil
|
|
154
|
+
best_idx = nil
|
|
155
|
+
best_dist = Float::INFINITY
|
|
156
|
+
|
|
157
|
+
@map.linedefs.each_with_index do |linedef, idx|
|
|
158
|
+
next if linedef.special == 0 # Skip non-special linedefs
|
|
159
|
+
|
|
160
|
+
v1 = @map.vertices[linedef.v1]
|
|
161
|
+
v2 = @map.vertices[linedef.v2]
|
|
162
|
+
|
|
163
|
+
# Check if player is close enough to the linedef
|
|
164
|
+
dist = point_to_line_distance(player_x, player_y, v1.x, v1.y, v2.x, v2.y)
|
|
165
|
+
next if dist > USE_DISTANCE
|
|
166
|
+
next if dist >= best_dist
|
|
167
|
+
|
|
168
|
+
# Check if player is facing the linedef (on the front side)
|
|
169
|
+
next unless facing_linedef?(player_x, player_y, cos_angle, sin_angle, v1, v2)
|
|
170
|
+
|
|
171
|
+
best_linedef = linedef
|
|
172
|
+
best_idx = idx
|
|
173
|
+
best_dist = dist
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
if best_linedef
|
|
177
|
+
@sector_actions.use_linedef(best_linedef, best_idx)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def point_to_line_distance(px, py, x1, y1, x2, y2)
|
|
182
|
+
# Vector from line start to point
|
|
183
|
+
dx = px - x1
|
|
184
|
+
dy = py - y1
|
|
185
|
+
|
|
186
|
+
# Line direction vector
|
|
187
|
+
line_dx = x2 - x1
|
|
188
|
+
line_dy = y2 - y1
|
|
189
|
+
line_len_sq = line_dx * line_dx + line_dy * line_dy
|
|
190
|
+
|
|
191
|
+
return Math.sqrt(dx * dx + dy * dy) if line_len_sq == 0
|
|
192
|
+
|
|
193
|
+
# Project point onto line, clamped to segment
|
|
194
|
+
t = ((dx * line_dx) + (dy * line_dy)) / line_len_sq
|
|
195
|
+
t = [[t, 0.0].max, 1.0].min
|
|
196
|
+
|
|
197
|
+
# Closest point on line segment
|
|
198
|
+
closest_x = x1 + t * line_dx
|
|
199
|
+
closest_y = y1 + t * line_dy
|
|
200
|
+
|
|
201
|
+
# Distance from point to closest point on segment
|
|
202
|
+
dist_x = px - closest_x
|
|
203
|
+
dist_y = py - closest_y
|
|
204
|
+
Math.sqrt(dist_x * dist_x + dist_y * dist_y)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def facing_linedef?(px, py, cos_angle, sin_angle, v1, v2)
|
|
208
|
+
# Calculate linedef normal (perpendicular to line, pointing to front side)
|
|
209
|
+
line_dx = v2.x - v1.x
|
|
210
|
+
line_dy = v2.y - v1.y
|
|
211
|
+
|
|
212
|
+
# Normal points to the right of the line direction
|
|
213
|
+
normal_x = -line_dy
|
|
214
|
+
normal_y = line_dx
|
|
215
|
+
|
|
216
|
+
# Normalize
|
|
217
|
+
len = Math.sqrt(normal_x * normal_x + normal_y * normal_y)
|
|
218
|
+
return false if len == 0
|
|
219
|
+
|
|
220
|
+
normal_x /= len
|
|
221
|
+
normal_y /= len
|
|
222
|
+
|
|
223
|
+
# Check if player is on the front side (normal side) of the line
|
|
224
|
+
to_player_x = px - v1.x
|
|
225
|
+
to_player_y = py - v1.y
|
|
226
|
+
dot_player = to_player_x * normal_x + to_player_y * normal_y
|
|
227
|
+
|
|
228
|
+
# Player must be on front side
|
|
229
|
+
return false if dot_player < 0
|
|
230
|
+
|
|
231
|
+
# Check if player is facing toward the line
|
|
232
|
+
dot_facing = cos_angle * (-normal_x) + sin_angle * (-normal_y)
|
|
233
|
+
dot_facing > 0.5 # Must be roughly facing the line
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def handle_weapon_switch
|
|
237
|
+
if Gosu.button_down?(Gosu::KB_1)
|
|
238
|
+
@player_state.switch_weapon(Game::PlayerState::WEAPON_FIST)
|
|
239
|
+
elsif Gosu.button_down?(Gosu::KB_2)
|
|
240
|
+
@player_state.switch_weapon(Game::PlayerState::WEAPON_PISTOL)
|
|
241
|
+
elsif Gosu.button_down?(Gosu::KB_3)
|
|
242
|
+
@player_state.switch_weapon(Game::PlayerState::WEAPON_SHOTGUN)
|
|
243
|
+
elsif Gosu.button_down?(Gosu::KB_4)
|
|
244
|
+
@player_state.switch_weapon(Game::PlayerState::WEAPON_CHAINGUN)
|
|
245
|
+
elsif Gosu.button_down?(Gosu::KB_5)
|
|
246
|
+
@player_state.switch_weapon(Game::PlayerState::WEAPON_ROCKET)
|
|
247
|
+
elsif Gosu.button_down?(Gosu::KB_6)
|
|
248
|
+
@player_state.switch_weapon(Game::PlayerState::WEAPON_PLASMA)
|
|
249
|
+
elsif Gosu.button_down?(Gosu::KB_7)
|
|
250
|
+
@player_state.switch_weapon(Game::PlayerState::WEAPON_BFG)
|
|
251
|
+
end
|
|
75
252
|
end
|
|
76
253
|
|
|
77
254
|
def try_move(dx, dy)
|