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
|
@@ -8,18 +8,30 @@ module Doom
|
|
|
8
8
|
STATUS_BAR_Y = SCREEN_HEIGHT - STATUS_BAR_HEIGHT
|
|
9
9
|
|
|
10
10
|
# DOOM status bar layout (from st_stuff.c)
|
|
11
|
-
#
|
|
12
|
-
# Y positions relative to status bar top
|
|
13
|
-
|
|
14
|
-
# Right edge X positions for numbers
|
|
11
|
+
# Positions from Chocolate Doom st_stuff.c (relative to status bar top)
|
|
15
12
|
AMMO_RIGHT_X = 44 # ST_AMMOX - right edge of 3-digit ammo
|
|
16
|
-
HEALTH_RIGHT_X = 90 # ST_HEALTHX
|
|
17
|
-
ARMOR_RIGHT_X = 221 # ST_ARMORX
|
|
13
|
+
HEALTH_RIGHT_X = 90 # ST_HEALTHX
|
|
14
|
+
ARMOR_RIGHT_X = 221 # ST_ARMORX
|
|
15
|
+
|
|
16
|
+
ARMS_BG_X = 104 # ST_ARMSBGX
|
|
17
|
+
ARMS_BG_Y = 0 # ST_ARMSBGY (relative to status bar)
|
|
18
|
+
ARMS_X = 111 # ST_ARMSX
|
|
19
|
+
ARMS_Y = 4 # ST_ARMSY (relative to status bar)
|
|
20
|
+
ARMS_XSPACE = 12
|
|
21
|
+
ARMS_YSPACE = 10
|
|
22
|
+
|
|
23
|
+
FACE_X = 149 # Centered in face background area
|
|
24
|
+
FACE_Y = 2 # Vertically centered in status bar
|
|
18
25
|
|
|
19
|
-
FACE_X = 149 # Adjusted for proper centering in face background
|
|
20
26
|
KEYS_X = 239 # ST_KEY0X
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
# Small ammo counts (right side of status bar)
|
|
29
|
+
SMALL_AMMO_X = 288 # Current ammo X
|
|
30
|
+
SMALL_MAX_X = 314 # Max ammo X
|
|
31
|
+
SMALL_AMMO_Y = [5, 11, 23, 17] # Bullets, Shells, Cells, Rockets (relative to bar)
|
|
32
|
+
|
|
33
|
+
NUM_WIDTH = 14 # Width of large digit
|
|
34
|
+
SMALL_NUM_WIDTH = 4 # Width of small digit
|
|
23
35
|
|
|
24
36
|
def initialize(hud_graphics, player_state)
|
|
25
37
|
@gfx = hud_graphics
|
|
@@ -32,25 +44,34 @@ module Doom
|
|
|
32
44
|
# Draw status bar background
|
|
33
45
|
draw_sprite(framebuffer, @gfx.status_bar, 0, STATUS_BAR_Y) if @gfx.status_bar
|
|
34
46
|
|
|
47
|
+
# Draw arms background (single-player only, replaces FRAG area)
|
|
48
|
+
draw_sprite(framebuffer, @gfx.arms_background, ARMS_BG_X, STATUS_BAR_Y + ARMS_BG_Y) if @gfx.arms_background
|
|
49
|
+
|
|
35
50
|
# Y position for numbers (3 pixels from top of status bar)
|
|
36
51
|
num_y = STATUS_BAR_Y + 3
|
|
37
52
|
|
|
38
53
|
# Draw ammo count (right-aligned ending at AMMO_RIGHT_X)
|
|
39
54
|
draw_number_right(framebuffer, @player.current_ammo, AMMO_RIGHT_X, num_y) if @player.current_ammo
|
|
40
55
|
|
|
41
|
-
# Draw health with percent
|
|
56
|
+
# Draw health with percent
|
|
42
57
|
draw_number_right(framebuffer, @player.health, HEALTH_RIGHT_X, num_y)
|
|
43
58
|
draw_percent(framebuffer, HEALTH_RIGHT_X, num_y)
|
|
44
59
|
|
|
60
|
+
# Draw weapon selector (2-7)
|
|
61
|
+
draw_arms(framebuffer)
|
|
62
|
+
|
|
45
63
|
# Draw face
|
|
46
64
|
draw_face(framebuffer)
|
|
47
65
|
|
|
48
|
-
# Draw armor with percent
|
|
66
|
+
# Draw armor with percent
|
|
49
67
|
draw_number_right(framebuffer, @player.armor, ARMOR_RIGHT_X, num_y)
|
|
50
68
|
draw_percent(framebuffer, ARMOR_RIGHT_X, num_y)
|
|
51
69
|
|
|
52
70
|
# Draw keys
|
|
53
71
|
draw_keys(framebuffer)
|
|
72
|
+
|
|
73
|
+
# Draw small ammo counts (right side)
|
|
74
|
+
draw_ammo_counts(framebuffer)
|
|
54
75
|
end
|
|
55
76
|
|
|
56
77
|
def update
|
|
@@ -113,27 +134,58 @@ module Doom
|
|
|
113
134
|
draw_sprite(framebuffer, percent, x, y) if percent
|
|
114
135
|
end
|
|
115
136
|
|
|
137
|
+
def draw_arms(framebuffer)
|
|
138
|
+
# Weapon numbers 2-7 in a 3x2 grid
|
|
139
|
+
6.times do |i|
|
|
140
|
+
weapon_num = i + 2 # weapons 2-7
|
|
141
|
+
owned = @player.has_weapons[weapon_num]
|
|
142
|
+
digit = owned ? @gfx.yellow_numbers[weapon_num] : @gfx.grey_numbers[weapon_num]
|
|
143
|
+
next unless digit
|
|
144
|
+
|
|
145
|
+
x = ARMS_X + (i % 3) * ARMS_XSPACE
|
|
146
|
+
y = STATUS_BAR_Y + ARMS_Y + (i / 3) * ARMS_YSPACE
|
|
147
|
+
draw_sprite(framebuffer, digit, x, y)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
116
151
|
def draw_face(framebuffer)
|
|
117
|
-
#
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return unless faces
|
|
152
|
+
# Pain level: 0 = healthy, 4 = near death
|
|
153
|
+
health = @player.health.clamp(0, 100)
|
|
154
|
+
pain_level = ((100 - health) * 5) / 101
|
|
121
155
|
|
|
122
|
-
# Get current face sprite
|
|
123
156
|
face = if @player.health <= 0
|
|
124
157
|
@gfx.faces[:dead]
|
|
125
|
-
elsif faces[:straight] && faces[:straight][@face_index]
|
|
126
|
-
faces[:straight][@face_index]
|
|
127
158
|
else
|
|
128
|
-
faces[
|
|
159
|
+
faces = @gfx.faces[pain_level]
|
|
160
|
+
faces[:straight][@face_index] if faces && faces[:straight]
|
|
129
161
|
end
|
|
130
162
|
|
|
131
163
|
return unless face
|
|
164
|
+
draw_sprite(framebuffer, face, FACE_X, STATUS_BAR_Y + FACE_Y)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def draw_ammo_counts(framebuffer)
|
|
168
|
+
ammo_current = [@player.ammo_bullets, @player.ammo_shells, @player.ammo_cells, @player.ammo_rockets]
|
|
169
|
+
ammo_max = [@player.max_bullets, @player.max_shells, @player.max_cells, @player.max_rockets]
|
|
170
|
+
|
|
171
|
+
4.times do |i|
|
|
172
|
+
y = STATUS_BAR_Y + SMALL_AMMO_Y[i]
|
|
173
|
+
draw_small_number_right(framebuffer, ammo_current[i], SMALL_AMMO_X, y)
|
|
174
|
+
draw_small_number_right(framebuffer, ammo_max[i], SMALL_MAX_X, y)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
132
177
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
178
|
+
def draw_small_number_right(framebuffer, value, right_x, y)
|
|
179
|
+
return unless value
|
|
180
|
+
str = value.to_i.to_s
|
|
181
|
+
current_x = right_x
|
|
182
|
+
str.reverse.each_char do |char|
|
|
183
|
+
digit = @gfx.yellow_numbers[char.to_i]
|
|
184
|
+
if digit
|
|
185
|
+
current_x -= SMALL_NUM_WIDTH
|
|
186
|
+
draw_sprite(framebuffer, digit, current_x, y)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
137
189
|
end
|
|
138
190
|
|
|
139
191
|
def draw_keys(framebuffer)
|
|
@@ -7,6 +7,14 @@ module Doom
|
|
|
7
7
|
# Weapon is rendered above the status bar
|
|
8
8
|
WEAPON_AREA_HEIGHT = SCREEN_HEIGHT - StatusBar::STATUS_BAR_HEIGHT
|
|
9
9
|
|
|
10
|
+
# DOOM positions weapon sprites using their built-in offsets:
|
|
11
|
+
# x = SCREENWIDTH/2 - sprite.left_offset
|
|
12
|
+
# y = WEAPONTOP + SCREENHEIGHT - 200 - sprite.top_offset
|
|
13
|
+
# WEAPONTOP = 32 in fixed-point = 32 pixels above the default position
|
|
14
|
+
# We scale for our 240px screen vs DOOM's 200px.
|
|
15
|
+
WEAPONTOP = 32
|
|
16
|
+
SCREEN_Y_OFFSET = SCREEN_HEIGHT - 200 # 40px offset for 240px screen
|
|
17
|
+
|
|
10
18
|
def initialize(hud_graphics, player_state)
|
|
11
19
|
@gfx = hud_graphics
|
|
12
20
|
@player = player_state
|
|
@@ -27,33 +35,21 @@ module Doom
|
|
|
27
35
|
|
|
28
36
|
return unless sprite
|
|
29
37
|
|
|
30
|
-
#
|
|
31
|
-
bob_x = @player.weapon_bob_x.to_i
|
|
32
|
-
bob_y = @player.weapon_bob_y.to_i
|
|
33
|
-
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
y = WEAPON_AREA_HEIGHT - sprite.height + clamped_bob_y
|
|
40
|
-
|
|
41
|
-
# Add some vertical offset during attack (recoil effect)
|
|
42
|
-
if @player.attacking
|
|
43
|
-
recoil = case @player.attack_frame
|
|
44
|
-
when 0 then -6
|
|
45
|
-
when 1 then -3
|
|
46
|
-
when 2 then 3
|
|
47
|
-
else 0
|
|
48
|
-
end
|
|
49
|
-
y += recoil
|
|
50
|
-
end
|
|
38
|
+
# Bob offset (frozen during attack to keep weapon steady)
|
|
39
|
+
bob_x = @player.attacking ? 0 : @player.weapon_bob_x.to_i
|
|
40
|
+
bob_y = @player.attacking ? 0 : @player.weapon_bob_y.to_i
|
|
41
|
+
|
|
42
|
+
# DOOM's R_DrawPSprite: x1 = centerx + (psp->sx - centerx - spriteoffset)
|
|
43
|
+
# With psp->sx = 1 (default): x = 1 - left_offset
|
|
44
|
+
# Uses sprite's built-in offsets for both weapon and flash alignment
|
|
45
|
+
x = 1 - sprite.left_offset + bob_x
|
|
46
|
+
y = 1 - sprite.top_offset + SCREEN_Y_OFFSET + bob_y
|
|
51
47
|
|
|
52
48
|
draw_weapon_sprite(framebuffer, sprite, x, y)
|
|
53
49
|
|
|
54
|
-
# Draw muzzle flash
|
|
55
|
-
if @player.attacking && @player.attack_frame
|
|
56
|
-
draw_muzzle_flash(framebuffer, weapon_name
|
|
50
|
+
# Draw muzzle flash only on the first fire frame (the actual shot)
|
|
51
|
+
if @player.attacking && @player.attack_frame == 0
|
|
52
|
+
draw_muzzle_flash(framebuffer, weapon_name)
|
|
57
53
|
end
|
|
58
54
|
end
|
|
59
55
|
|
|
@@ -83,7 +79,7 @@ module Doom
|
|
|
83
79
|
end
|
|
84
80
|
end
|
|
85
81
|
|
|
86
|
-
def draw_muzzle_flash(framebuffer, weapon_name
|
|
82
|
+
def draw_muzzle_flash(framebuffer, weapon_name)
|
|
87
83
|
weapon_data = @gfx.weapons[weapon_name]
|
|
88
84
|
return unless weapon_data && weapon_data[:flash]
|
|
89
85
|
|
|
@@ -91,9 +87,10 @@ module Doom
|
|
|
91
87
|
flash_sprite = weapon_data[:flash][flash_frame]
|
|
92
88
|
return unless flash_sprite
|
|
93
89
|
|
|
94
|
-
# Flash
|
|
95
|
-
|
|
96
|
-
|
|
90
|
+
# Flash uses same positioning as weapon sprite (built-in offsets)
|
|
91
|
+
# Same positioning formula as weapon sprite
|
|
92
|
+
flash_x = 1 - flash_sprite.left_offset
|
|
93
|
+
flash_y = 1 - flash_sprite.top_offset + SCREEN_Y_OFFSET
|
|
97
94
|
|
|
98
95
|
draw_weapon_sprite(framebuffer, flash_sprite, flash_x, flash_y)
|
|
99
96
|
end
|
data/lib/doom/version.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Doom
|
|
|
4
4
|
module Wad
|
|
5
5
|
# Loads HUD graphics (status bar, weapons) from WAD
|
|
6
6
|
class HudGraphics
|
|
7
|
-
attr_reader :status_bar, :numbers, :weapons, :faces, :keys
|
|
7
|
+
attr_reader :status_bar, :arms_background, :numbers, :grey_numbers, :yellow_numbers, :weapons, :faces, :keys
|
|
8
8
|
|
|
9
9
|
def initialize(wad)
|
|
10
10
|
@wad = wad
|
|
@@ -84,6 +84,7 @@ module Doom
|
|
|
84
84
|
|
|
85
85
|
def load_status_bar
|
|
86
86
|
@status_bar = load_graphic('STBAR')
|
|
87
|
+
@arms_background = load_graphic('STARMS')
|
|
87
88
|
end
|
|
88
89
|
|
|
89
90
|
def load_numbers
|
|
@@ -95,11 +96,17 @@ module Doom
|
|
|
95
96
|
@numbers['-'] = load_graphic('STTMINUS')
|
|
96
97
|
@numbers['%'] = load_graphic('STTPRCNT')
|
|
97
98
|
|
|
98
|
-
# Small grey numbers for arms
|
|
99
|
+
# Small grey numbers for arms (weapon not owned)
|
|
99
100
|
@grey_numbers = {}
|
|
100
101
|
(0..9).each do |n|
|
|
101
102
|
@grey_numbers[n] = load_graphic("STGNUM#{n}")
|
|
102
103
|
end
|
|
104
|
+
|
|
105
|
+
# Small yellow numbers for arms (weapon owned) and ammo counts
|
|
106
|
+
@yellow_numbers = {}
|
|
107
|
+
(0..9).each do |n|
|
|
108
|
+
@yellow_numbers[n] = load_graphic("STYSNUM#{n}")
|
|
109
|
+
end
|
|
103
110
|
end
|
|
104
111
|
|
|
105
112
|
def load_weapons
|
|
@@ -129,6 +136,67 @@ module Doom
|
|
|
129
136
|
load_graphic('PUNGD0')
|
|
130
137
|
].compact
|
|
131
138
|
}
|
|
139
|
+
|
|
140
|
+
# Shotgun (SHTG)
|
|
141
|
+
@weapons[:shotgun] = {
|
|
142
|
+
idle: load_graphic('SHTGA0'),
|
|
143
|
+
fire: [
|
|
144
|
+
load_graphic('SHTGB0'),
|
|
145
|
+
load_graphic('SHTGC0'),
|
|
146
|
+
load_graphic('SHTGD0')
|
|
147
|
+
].compact,
|
|
148
|
+
flash: [load_graphic('SHTFA0'), load_graphic('SHTFB0')].compact
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# Chaingun (CHGG)
|
|
152
|
+
@weapons[:chaingun] = {
|
|
153
|
+
idle: load_graphic('CHGGA0'),
|
|
154
|
+
fire: [
|
|
155
|
+
load_graphic('CHGGB0'),
|
|
156
|
+
load_graphic('CHGGC0')
|
|
157
|
+
].compact,
|
|
158
|
+
flash: [load_graphic('CHGFA0'), load_graphic('CHGFB0')].compact
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Rocket launcher (MISG)
|
|
162
|
+
@weapons[:rocket] = {
|
|
163
|
+
idle: load_graphic('MISGA0'),
|
|
164
|
+
fire: [
|
|
165
|
+
load_graphic('MISGB0'),
|
|
166
|
+
load_graphic('MISGC0'),
|
|
167
|
+
load_graphic('MISGD0')
|
|
168
|
+
].compact,
|
|
169
|
+
flash: [load_graphic('MISFA0'), load_graphic('MISFB0'), load_graphic('MISFC0')].compact
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# Plasma rifle (PLSG)
|
|
173
|
+
@weapons[:plasma] = {
|
|
174
|
+
idle: load_graphic('PLSGA0'),
|
|
175
|
+
fire: [
|
|
176
|
+
load_graphic('PLSGB0')
|
|
177
|
+
].compact,
|
|
178
|
+
flash: [load_graphic('PLSFA0'), load_graphic('PLSFB0')].compact
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# BFG9000 (BFGG)
|
|
182
|
+
@weapons[:bfg] = {
|
|
183
|
+
idle: load_graphic('BFGGA0'),
|
|
184
|
+
fire: [
|
|
185
|
+
load_graphic('BFGGB0'),
|
|
186
|
+
load_graphic('BFGGC0')
|
|
187
|
+
].compact,
|
|
188
|
+
flash: [load_graphic('BFGFA0'), load_graphic('BFGFB0')].compact
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# Chainsaw (SAWG)
|
|
192
|
+
@weapons[:chainsaw] = {
|
|
193
|
+
idle: load_graphic('SAWGA0'),
|
|
194
|
+
fire: [
|
|
195
|
+
load_graphic('SAWGB0'),
|
|
196
|
+
load_graphic('SAWGC0'),
|
|
197
|
+
load_graphic('SAWGD0')
|
|
198
|
+
].compact
|
|
199
|
+
}
|
|
132
200
|
end
|
|
133
201
|
|
|
134
202
|
def load_faces
|
data/lib/doom/wad/sprite.rb
CHANGED
|
@@ -140,6 +140,11 @@ module Doom
|
|
|
140
140
|
@wad = wad
|
|
141
141
|
@cache = {}
|
|
142
142
|
@rotation_cache = {}
|
|
143
|
+
|
|
144
|
+
# Build sprite lump index: maps "PREFIXframe_rotation" -> [lump_name, mirrored?]
|
|
145
|
+
# Handles combined lumps like SPOSA2A8 (rotation 2 normal, rotation 8 mirrored)
|
|
146
|
+
@sprite_index = {}
|
|
147
|
+
build_sprite_index
|
|
143
148
|
end
|
|
144
149
|
|
|
145
150
|
# Get default sprite (rotation 0 or 1)
|
|
@@ -149,9 +154,8 @@ module Doom
|
|
|
149
154
|
prefix = THING_SPRITES[thing_type]
|
|
150
155
|
return nil unless prefix
|
|
151
156
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
Sprite.load(@wad, "#{prefix}A1")
|
|
157
|
+
sprite = load_sprite_frame(prefix, 'A', 0) ||
|
|
158
|
+
load_sprite_frame(prefix, 'A', 1)
|
|
155
159
|
|
|
156
160
|
@cache[thing_type] = sprite
|
|
157
161
|
sprite
|
|
@@ -169,36 +173,106 @@ module Doom
|
|
|
169
173
|
prefix = THING_SPRITES[thing_type]
|
|
170
174
|
return nil unless prefix
|
|
171
175
|
|
|
172
|
-
# Check
|
|
173
|
-
|
|
174
|
-
if
|
|
175
|
-
return @rotation_cache[cache_key] if @rotation_cache[cache_key]
|
|
176
|
-
else
|
|
177
|
-
sprite = Sprite.load(@wad, cache_key)
|
|
178
|
-
@rotation_cache[cache_key] = sprite
|
|
179
|
-
return sprite if sprite
|
|
180
|
-
end
|
|
176
|
+
# Check for rotation 0 (all angles) sprite first
|
|
177
|
+
sprite = load_sprite_frame(prefix, 'A', 0)
|
|
178
|
+
return sprite if sprite
|
|
181
179
|
|
|
182
180
|
# Calculate rotation frame (1-8)
|
|
183
|
-
#
|
|
184
|
-
#
|
|
185
|
-
angle_diff = viewer_angle - (thing_angle * Math::PI / 180.0)
|
|
186
|
-
|
|
187
|
-
# Normalize to 0-2π
|
|
181
|
+
# DOOM: rot = (R_PointToAngle(thing) - thing->angle + ANG45/2*9) >> 29
|
|
182
|
+
# Rotation 1=front (viewer faces monster's front), 5=back
|
|
183
|
+
angle_diff = viewer_angle - (thing_angle * Math::PI / 180.0) + Math::PI
|
|
188
184
|
angle_diff = angle_diff % (2 * Math::PI)
|
|
189
185
|
angle_diff += 2 * Math::PI if angle_diff < 0
|
|
186
|
+
rotation = ((angle_diff + Math::PI / 8) / (Math::PI / 4)).to_i % 8 + 1
|
|
187
|
+
|
|
188
|
+
load_sprite_frame(prefix, 'A', rotation) || @cache[thing_type]
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Get a specific frame (for death animations, etc.)
|
|
192
|
+
def get_frame(thing_type, frame_letter, viewer_angle, thing_angle)
|
|
193
|
+
prefix = THING_SPRITES[thing_type]
|
|
194
|
+
return nil unless prefix
|
|
195
|
+
|
|
196
|
+
# Death frames typically use rotation 0 (same from all angles)
|
|
197
|
+
sprite = load_sprite_frame(prefix, frame_letter, 0)
|
|
198
|
+
return sprite if sprite
|
|
190
199
|
|
|
191
|
-
#
|
|
192
|
-
|
|
193
|
-
|
|
200
|
+
# Try with calculated rotation (same formula as get_rotated)
|
|
201
|
+
angle_diff = viewer_angle - (thing_angle * Math::PI / 180.0) + Math::PI
|
|
202
|
+
angle_diff = angle_diff % (2 * Math::PI)
|
|
203
|
+
angle_diff += 2 * Math::PI if angle_diff < 0
|
|
194
204
|
rotation = ((angle_diff + Math::PI / 8) / (Math::PI / 4)).to_i % 8 + 1
|
|
195
205
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
206
|
+
load_sprite_frame(prefix, frame_letter, rotation)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
private
|
|
210
|
+
|
|
211
|
+
def build_sprite_index
|
|
212
|
+
@wad.directory.each do |entry|
|
|
213
|
+
name = entry.name
|
|
214
|
+
next if name.length < 6
|
|
215
|
+
|
|
216
|
+
prefix = name[0, 4]
|
|
217
|
+
frame1 = name[4]
|
|
218
|
+
rot1 = name[5].to_i
|
|
219
|
+
|
|
220
|
+
# Register first frame+rotation
|
|
221
|
+
key = "#{prefix}#{frame1}#{rot1}"
|
|
222
|
+
@sprite_index[key] = [name, false]
|
|
223
|
+
|
|
224
|
+
# Check for mirrored second rotation (e.g., SPOSA2A8)
|
|
225
|
+
if name.length >= 8
|
|
226
|
+
frame2 = name[6]
|
|
227
|
+
rot2 = name[7].to_i
|
|
228
|
+
key2 = "#{prefix}#{frame2}#{rot2}"
|
|
229
|
+
@sprite_index[key2] = [name, true]
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def load_sprite_frame(prefix, frame, rotation)
|
|
235
|
+
key = "#{prefix}#{frame}#{rotation}"
|
|
236
|
+
return @rotation_cache[key] if @rotation_cache.key?(key)
|
|
237
|
+
|
|
238
|
+
index_entry = @sprite_index[key]
|
|
239
|
+
unless index_entry
|
|
240
|
+
@rotation_cache[key] = nil
|
|
241
|
+
return nil
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
lump_name, mirrored = index_entry
|
|
245
|
+
# Load the base sprite (may be shared by mirrored pair)
|
|
246
|
+
base = load_or_cache_lump(lump_name)
|
|
247
|
+
unless base
|
|
248
|
+
@rotation_cache[key] = nil
|
|
249
|
+
return nil
|
|
199
250
|
end
|
|
200
251
|
|
|
201
|
-
|
|
252
|
+
sprite = mirrored ? mirror_sprite(base) : base
|
|
253
|
+
@rotation_cache[key] = sprite
|
|
254
|
+
sprite
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def load_or_cache_lump(lump_name)
|
|
258
|
+
cache_key = "_lump_#{lump_name}"
|
|
259
|
+
return @rotation_cache[cache_key] if @rotation_cache.key?(cache_key)
|
|
260
|
+
|
|
261
|
+
sprite = Sprite.load(@wad, lump_name)
|
|
262
|
+
@rotation_cache[cache_key] = sprite
|
|
263
|
+
sprite
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def mirror_sprite(sprite)
|
|
267
|
+
# Flip columns horizontally, adjust left_offset
|
|
268
|
+
mirrored_columns = sprite.instance_variable_get(:@columns).reverse
|
|
269
|
+
mirrored_left = sprite.width - sprite.left_offset
|
|
270
|
+
Sprite.new(
|
|
271
|
+
"#{sprite.name}_M",
|
|
272
|
+
sprite.width, sprite.height,
|
|
273
|
+
mirrored_left, sprite.top_offset,
|
|
274
|
+
mirrored_columns
|
|
275
|
+
)
|
|
202
276
|
end
|
|
203
277
|
end
|
|
204
278
|
end
|
data/lib/doom/wad/texture.rb
CHANGED
|
@@ -17,6 +17,7 @@ module Doom
|
|
|
17
17
|
def self.load_all(wad)
|
|
18
18
|
pnames = load_pnames(wad)
|
|
19
19
|
textures = {}
|
|
20
|
+
texture_names = []
|
|
20
21
|
|
|
21
22
|
%w[TEXTURE1 TEXTURE2].each do |lump_name|
|
|
22
23
|
data = wad.read_lump(lump_name)
|
|
@@ -24,10 +25,11 @@ module Doom
|
|
|
24
25
|
|
|
25
26
|
parse_texture_lump(data).each do |tex|
|
|
26
27
|
textures[tex.name] = tex
|
|
28
|
+
texture_names << tex.name
|
|
27
29
|
end
|
|
28
30
|
end
|
|
29
31
|
|
|
30
|
-
{ textures: textures, pnames: pnames }
|
|
32
|
+
{ textures: textures, pnames: pnames, texture_names: texture_names }
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
def self.load_pnames(wad)
|
|
@@ -71,13 +73,14 @@ module Doom
|
|
|
71
73
|
end
|
|
72
74
|
|
|
73
75
|
class TextureManager
|
|
74
|
-
attr_reader :textures, :pnames, :patches
|
|
76
|
+
attr_reader :textures, :pnames, :patches, :texture_names
|
|
75
77
|
|
|
76
78
|
def initialize(wad)
|
|
77
79
|
@wad = wad
|
|
78
80
|
result = Texture.load_all(wad)
|
|
79
81
|
@textures = result[:textures]
|
|
80
82
|
@pnames = result[:pnames]
|
|
83
|
+
@texture_names = result[:texture_names]
|
|
81
84
|
@patches = {}
|
|
82
85
|
@composite_cache = {}
|
|
83
86
|
end
|
|
@@ -132,21 +135,29 @@ module Doom
|
|
|
132
135
|
@width = width
|
|
133
136
|
@height = height
|
|
134
137
|
@columns = columns
|
|
138
|
+
@column_cache = Array.new(width)
|
|
139
|
+
precompute_columns
|
|
135
140
|
end
|
|
136
141
|
|
|
137
|
-
def column_pixels(x,
|
|
138
|
-
x
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
def column_pixels(x, _height_needed = nil)
|
|
143
|
+
@column_cache[x & (@width - 1)]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
141
147
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
148
|
+
def precompute_columns
|
|
149
|
+
@width.times do |x|
|
|
150
|
+
posts = @columns[x]
|
|
151
|
+
pixels = Array.new(@height, 0)
|
|
152
|
+
posts.each do |post|
|
|
153
|
+
td = post.top_delta
|
|
154
|
+
post.pixels.each_with_index do |color, i|
|
|
155
|
+
y = (td + i) % @height
|
|
156
|
+
pixels[y] = color
|
|
157
|
+
end
|
|
147
158
|
end
|
|
159
|
+
@column_cache[x] = pixels.freeze
|
|
148
160
|
end
|
|
149
|
-
pixels
|
|
150
161
|
end
|
|
151
162
|
end
|
|
152
163
|
end
|
data/lib/doom/wad_downloader.rb
CHANGED
|
@@ -66,7 +66,16 @@ module Doom
|
|
|
66
66
|
uri = URI.parse(SHAREWARE_URL)
|
|
67
67
|
downloaded = 0
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
ssl_opts = { use_ssl: uri.scheme == 'https' }
|
|
70
|
+
# Workaround for SSL certificate issues on some systems
|
|
71
|
+
begin
|
|
72
|
+
Net::HTTP.start(uri.host, uri.port, **ssl_opts) { |h| h.head(uri) }
|
|
73
|
+
rescue OpenSSL::SSL::SSLError
|
|
74
|
+
puts "SSL certificate verification failed, retrying without verification..."
|
|
75
|
+
ssl_opts[:verify_mode] = OpenSSL::SSL::VERIFY_NONE
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
Net::HTTP.start(uri.host, uri.port, **ssl_opts) do |http|
|
|
70
79
|
request = Net::HTTP::Get.new(uri)
|
|
71
80
|
|
|
72
81
|
http.request(request) do |response|
|
data/lib/doom.rb
CHANGED
|
@@ -13,6 +13,11 @@ require_relative 'doom/wad/hud_graphics'
|
|
|
13
13
|
require_relative 'doom/map/data'
|
|
14
14
|
require_relative 'doom/game/player_state'
|
|
15
15
|
require_relative 'doom/game/sector_actions'
|
|
16
|
+
require_relative 'doom/game/animations'
|
|
17
|
+
require_relative 'doom/game/item_pickup'
|
|
18
|
+
require_relative 'doom/game/combat'
|
|
19
|
+
require_relative 'doom/game/sector_effects'
|
|
20
|
+
require_relative 'doom/game/monster_ai'
|
|
16
21
|
require_relative 'doom/render/renderer'
|
|
17
22
|
require_relative 'doom/render/status_bar'
|
|
18
23
|
require_relative 'doom/render/weapon_renderer'
|
|
@@ -61,8 +66,12 @@ module Doom
|
|
|
61
66
|
player_start = Map::Thing.new(0, 0, 90, 1, 0)
|
|
62
67
|
end
|
|
63
68
|
|
|
69
|
+
puts 'Initializing animations...'
|
|
70
|
+
flat_names = flats.map(&:name)
|
|
71
|
+
animations = Game::Animations.new(textures.texture_names, flat_names)
|
|
72
|
+
|
|
64
73
|
puts 'Creating renderer...'
|
|
65
|
-
renderer = Render::Renderer.new(wad, map, textures, palette, colormap, flats, sprites)
|
|
74
|
+
renderer = Render::Renderer.new(wad, map, textures, palette, colormap, flats, sprites, animations)
|
|
66
75
|
renderer.set_player(player_start.x, player_start.y, 41, player_start.angle)
|
|
67
76
|
|
|
68
77
|
puts 'Setting up player state and HUD...'
|
|
@@ -70,9 +79,13 @@ module Doom
|
|
|
70
79
|
status_bar = Render::StatusBar.new(hud_graphics, player_state)
|
|
71
80
|
weapon_renderer = Render::WeaponRenderer.new(hud_graphics, player_state)
|
|
72
81
|
sector_actions = Game::SectorActions.new(map)
|
|
82
|
+
sector_effects = Game::SectorEffects.new(map)
|
|
83
|
+
item_pickup = Game::ItemPickup.new(map, player_state)
|
|
84
|
+
combat = Game::Combat.new(map, player_state, sprites)
|
|
85
|
+
monster_ai = Game::MonsterAI.new(map, combat)
|
|
73
86
|
|
|
74
87
|
puts 'Starting game window...'
|
|
75
|
-
window = Platform::GosuWindow.new(renderer, palette, map, player_state, status_bar, weapon_renderer, sector_actions)
|
|
88
|
+
window = Platform::GosuWindow.new(renderer, palette, map, player_state, status_bar, weapon_renderer, sector_actions, animations, sector_effects, item_pickup, combat, monster_ai)
|
|
76
89
|
window.show
|
|
77
90
|
end
|
|
78
91
|
end
|