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
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Render
|
|
5
|
+
# Renders the classic DOOM status bar at the bottom of the screen
|
|
6
|
+
class StatusBar
|
|
7
|
+
STATUS_BAR_HEIGHT = 32
|
|
8
|
+
STATUS_BAR_Y = SCREEN_HEIGHT - STATUS_BAR_HEIGHT
|
|
9
|
+
|
|
10
|
+
# DOOM status bar layout (from st_stuff.c)
|
|
11
|
+
# Positions from Chocolate Doom st_stuff.c (relative to status bar top)
|
|
12
|
+
AMMO_RIGHT_X = 44 # ST_AMMOX - right edge of 3-digit ammo
|
|
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
|
|
25
|
+
|
|
26
|
+
KEYS_X = 239 # ST_KEY0X
|
|
27
|
+
|
|
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
|
|
35
|
+
|
|
36
|
+
def initialize(hud_graphics, player_state)
|
|
37
|
+
@gfx = hud_graphics
|
|
38
|
+
@player = player_state
|
|
39
|
+
@face_timer = 0
|
|
40
|
+
@face_index = 0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def render(framebuffer)
|
|
44
|
+
# Draw status bar background
|
|
45
|
+
draw_sprite(framebuffer, @gfx.status_bar, 0, STATUS_BAR_Y) if @gfx.status_bar
|
|
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
|
+
|
|
50
|
+
# Y position for numbers (3 pixels from top of status bar)
|
|
51
|
+
num_y = STATUS_BAR_Y + 3
|
|
52
|
+
|
|
53
|
+
# Draw ammo count (right-aligned ending at AMMO_RIGHT_X)
|
|
54
|
+
draw_number_right(framebuffer, @player.current_ammo, AMMO_RIGHT_X, num_y) if @player.current_ammo
|
|
55
|
+
|
|
56
|
+
# Draw health with percent
|
|
57
|
+
draw_number_right(framebuffer, @player.health, HEALTH_RIGHT_X, num_y)
|
|
58
|
+
draw_percent(framebuffer, HEALTH_RIGHT_X, num_y)
|
|
59
|
+
|
|
60
|
+
# Draw weapon selector (2-7)
|
|
61
|
+
draw_arms(framebuffer)
|
|
62
|
+
|
|
63
|
+
# Draw face
|
|
64
|
+
draw_face(framebuffer)
|
|
65
|
+
|
|
66
|
+
# Draw armor with percent
|
|
67
|
+
draw_number_right(framebuffer, @player.armor, ARMOR_RIGHT_X, num_y)
|
|
68
|
+
draw_percent(framebuffer, ARMOR_RIGHT_X, num_y)
|
|
69
|
+
|
|
70
|
+
# Draw keys
|
|
71
|
+
draw_keys(framebuffer)
|
|
72
|
+
|
|
73
|
+
# Draw small ammo counts (right side)
|
|
74
|
+
draw_ammo_counts(framebuffer)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def update
|
|
78
|
+
# Cycle face animation
|
|
79
|
+
@face_timer += 1
|
|
80
|
+
if @face_timer > 15 # Change face every ~0.5 seconds
|
|
81
|
+
@face_timer = 0
|
|
82
|
+
@face_index = (@face_index + 1) % 3
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def draw_sprite(framebuffer, sprite, x, y)
|
|
89
|
+
return unless sprite
|
|
90
|
+
|
|
91
|
+
sprite.width.times do |sx|
|
|
92
|
+
column = sprite.column_pixels(sx)
|
|
93
|
+
next unless column
|
|
94
|
+
|
|
95
|
+
draw_x = x + sx
|
|
96
|
+
next if draw_x < 0 || draw_x >= SCREEN_WIDTH
|
|
97
|
+
|
|
98
|
+
column.each_with_index do |color, sy|
|
|
99
|
+
next unless color
|
|
100
|
+
|
|
101
|
+
draw_y = y + sy
|
|
102
|
+
next if draw_y < 0 || draw_y >= SCREEN_HEIGHT
|
|
103
|
+
|
|
104
|
+
framebuffer[draw_y * SCREEN_WIDTH + draw_x] = color
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Draw number right-aligned with right edge at right_x
|
|
110
|
+
def draw_number_right(framebuffer, value, right_x, y)
|
|
111
|
+
return unless value
|
|
112
|
+
|
|
113
|
+
value = value.to_i.clamp(-999, 999)
|
|
114
|
+
str = value.to_s
|
|
115
|
+
|
|
116
|
+
# Draw from right to left, starting from right edge
|
|
117
|
+
current_x = right_x
|
|
118
|
+
str.reverse.each_char do |char|
|
|
119
|
+
digit_sprite = if char == '-'
|
|
120
|
+
@gfx.numbers['-']
|
|
121
|
+
else
|
|
122
|
+
@gfx.numbers[char.to_i]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
if digit_sprite
|
|
126
|
+
current_x -= NUM_WIDTH
|
|
127
|
+
draw_sprite(framebuffer, digit_sprite, current_x, y)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def draw_percent(framebuffer, x, y)
|
|
133
|
+
percent = @gfx.numbers['%']
|
|
134
|
+
draw_sprite(framebuffer, percent, x, y) if percent
|
|
135
|
+
end
|
|
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
|
+
|
|
151
|
+
def draw_face(framebuffer)
|
|
152
|
+
# Pain level: 0 = healthy, 4 = near death
|
|
153
|
+
health = @player.health.clamp(0, 100)
|
|
154
|
+
pain_level = ((100 - health) * 5) / 101
|
|
155
|
+
|
|
156
|
+
face = if @player.health <= 0
|
|
157
|
+
@gfx.faces[:dead]
|
|
158
|
+
else
|
|
159
|
+
faces = @gfx.faces[pain_level]
|
|
160
|
+
faces[:straight][@face_index] if faces && faces[:straight]
|
|
161
|
+
end
|
|
162
|
+
|
|
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
|
|
177
|
+
|
|
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
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def draw_keys(framebuffer)
|
|
192
|
+
key_x = KEYS_X
|
|
193
|
+
key_spacing = 10
|
|
194
|
+
|
|
195
|
+
# Blue keys (top row)
|
|
196
|
+
if @player.keys[:blue_card]
|
|
197
|
+
draw_sprite(framebuffer, @gfx.keys[:blue_card], key_x, STATUS_BAR_Y + 3)
|
|
198
|
+
elsif @player.keys[:blue_skull]
|
|
199
|
+
draw_sprite(framebuffer, @gfx.keys[:blue_skull], key_x, STATUS_BAR_Y + 3)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Yellow keys (middle row)
|
|
203
|
+
if @player.keys[:yellow_card]
|
|
204
|
+
draw_sprite(framebuffer, @gfx.keys[:yellow_card], key_x, STATUS_BAR_Y + 13)
|
|
205
|
+
elsif @player.keys[:yellow_skull]
|
|
206
|
+
draw_sprite(framebuffer, @gfx.keys[:yellow_skull], key_x, STATUS_BAR_Y + 13)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Red keys (bottom row)
|
|
210
|
+
if @player.keys[:red_card]
|
|
211
|
+
draw_sprite(framebuffer, @gfx.keys[:red_card], key_x, STATUS_BAR_Y + 23)
|
|
212
|
+
elsif @player.keys[:red_skull]
|
|
213
|
+
draw_sprite(framebuffer, @gfx.keys[:red_skull], key_x, STATUS_BAR_Y + 23)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Render
|
|
5
|
+
# Renders the first-person weapon view
|
|
6
|
+
class WeaponRenderer
|
|
7
|
+
# Weapon is rendered above the status bar
|
|
8
|
+
WEAPON_AREA_HEIGHT = SCREEN_HEIGHT - StatusBar::STATUS_BAR_HEIGHT
|
|
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
|
+
|
|
18
|
+
def initialize(hud_graphics, player_state)
|
|
19
|
+
@gfx = hud_graphics
|
|
20
|
+
@player = player_state
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def render(framebuffer)
|
|
24
|
+
weapon_name = @player.weapon_name
|
|
25
|
+
weapon_data = @gfx.weapons[weapon_name]
|
|
26
|
+
return unless weapon_data
|
|
27
|
+
|
|
28
|
+
# Get the appropriate frame
|
|
29
|
+
sprite = if @player.attacking && weapon_data[:fire]
|
|
30
|
+
frame = @player.attack_frame.clamp(0, weapon_data[:fire].length - 1)
|
|
31
|
+
weapon_data[:fire][frame]
|
|
32
|
+
else
|
|
33
|
+
weapon_data[:idle]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
return unless sprite
|
|
37
|
+
|
|
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
|
|
47
|
+
|
|
48
|
+
draw_weapon_sprite(framebuffer, sprite, x, y)
|
|
49
|
+
|
|
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)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def draw_weapon_sprite(framebuffer, sprite, base_x, base_y)
|
|
59
|
+
return unless sprite
|
|
60
|
+
|
|
61
|
+
# Clip to screen bounds (don't draw over status bar)
|
|
62
|
+
max_y = WEAPON_AREA_HEIGHT - 1
|
|
63
|
+
|
|
64
|
+
sprite.width.times do |sx|
|
|
65
|
+
column = sprite.column_pixels(sx)
|
|
66
|
+
next unless column
|
|
67
|
+
|
|
68
|
+
draw_x = base_x + sx
|
|
69
|
+
next if draw_x < 0 || draw_x >= SCREEN_WIDTH
|
|
70
|
+
|
|
71
|
+
column.each_with_index do |color, sy|
|
|
72
|
+
next unless color # Skip transparent pixels
|
|
73
|
+
|
|
74
|
+
draw_y = base_y + sy
|
|
75
|
+
next if draw_y < 0 || draw_y > max_y
|
|
76
|
+
|
|
77
|
+
framebuffer[draw_y * SCREEN_WIDTH + draw_x] = color
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def draw_muzzle_flash(framebuffer, weapon_name)
|
|
83
|
+
weapon_data = @gfx.weapons[weapon_name]
|
|
84
|
+
return unless weapon_data && weapon_data[:flash]
|
|
85
|
+
|
|
86
|
+
flash_frame = @player.attack_frame.clamp(0, weapon_data[:flash].length - 1)
|
|
87
|
+
flash_sprite = weapon_data[:flash][flash_frame]
|
|
88
|
+
return unless flash_sprite
|
|
89
|
+
|
|
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
|
|
94
|
+
|
|
95
|
+
draw_weapon_sprite(framebuffer, flash_sprite, flash_x, flash_y)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
data/lib/doom/version.rb
CHANGED
data/lib/doom/wad/colormap.rb
CHANGED
|
@@ -16,12 +16,6 @@ module Doom
|
|
|
16
16
|
@maps[map_index]
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def map_color(color_index, light_level)
|
|
20
|
-
map_index = 31 - (light_level >> 3)
|
|
21
|
-
map_index = map_index.clamp(0, 31)
|
|
22
|
-
@maps[map_index][color_index]
|
|
23
|
-
end
|
|
24
|
-
|
|
25
19
|
def self.load(wad)
|
|
26
20
|
data = wad.read_lump('COLORMAP')
|
|
27
21
|
raise Error, 'COLORMAP lump not found' unless data
|
data/lib/doom/wad/flat.rb
CHANGED
|
@@ -26,27 +26,6 @@ module Doom
|
|
|
26
26
|
@pixels[(y & 63) * WIDTH + (x & 63)]
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def to_png(palette, filename)
|
|
30
|
-
require 'chunky_png'
|
|
31
|
-
|
|
32
|
-
img = ChunkyPNG::Image.new(WIDTH, HEIGHT)
|
|
33
|
-
@pixels.each_with_index do |color_index, i|
|
|
34
|
-
x = i % WIDTH
|
|
35
|
-
y = i / WIDTH
|
|
36
|
-
r, g, b = palette[color_index]
|
|
37
|
-
img[x, y] = ChunkyPNG::Color.rgb(r, g, b)
|
|
38
|
-
end
|
|
39
|
-
img.save(filename)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def self.load(wad, name)
|
|
43
|
-
data = wad.read_lump(name)
|
|
44
|
-
return nil unless data
|
|
45
|
-
raise Error, "Invalid flat size: #{data.size}" unless data.size == SIZE
|
|
46
|
-
|
|
47
|
-
new(name, data.bytes)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
29
|
def self.load_all(wad)
|
|
51
30
|
entries = wad.lumps_between('F_START', 'F_END')
|
|
52
31
|
entries.map do |entry|
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Wad
|
|
5
|
+
# Loads HUD graphics (status bar, weapons) from WAD
|
|
6
|
+
class HudGraphics
|
|
7
|
+
attr_reader :status_bar, :arms_background, :numbers, :grey_numbers, :yellow_numbers, :weapons, :faces, :keys
|
|
8
|
+
|
|
9
|
+
def initialize(wad)
|
|
10
|
+
@wad = wad
|
|
11
|
+
@cache = {}
|
|
12
|
+
|
|
13
|
+
load_status_bar
|
|
14
|
+
load_numbers
|
|
15
|
+
load_weapons
|
|
16
|
+
load_faces
|
|
17
|
+
load_keys
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Get a cached graphic by name
|
|
21
|
+
def [](name)
|
|
22
|
+
@cache[name]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def load_graphic(name)
|
|
28
|
+
return @cache[name] if @cache[name]
|
|
29
|
+
|
|
30
|
+
entry = @wad.find_lump(name)
|
|
31
|
+
return nil unless entry
|
|
32
|
+
|
|
33
|
+
data = @wad.read_lump_at(entry)
|
|
34
|
+
return nil unless data && data.size > 8
|
|
35
|
+
|
|
36
|
+
sprite = parse_patch(name, data)
|
|
37
|
+
@cache[name] = sprite
|
|
38
|
+
sprite
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def parse_patch(name, data)
|
|
42
|
+
width = data[0, 2].unpack1('v')
|
|
43
|
+
height = data[2, 2].unpack1('v')
|
|
44
|
+
left_offset = data[4, 2].unpack1('s<')
|
|
45
|
+
top_offset = data[6, 2].unpack1('s<')
|
|
46
|
+
|
|
47
|
+
# Read column offsets
|
|
48
|
+
column_offsets = width.times.map do |i|
|
|
49
|
+
data[8 + i * 4, 4].unpack1('V')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Build column data
|
|
53
|
+
columns = column_offsets.map do |offset|
|
|
54
|
+
read_column(data, offset, height)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
HudSprite.new(name, width, height, left_offset, top_offset, columns)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def read_column(data, offset, height)
|
|
61
|
+
pixels = Array.new(height)
|
|
62
|
+
pos = offset
|
|
63
|
+
|
|
64
|
+
loop do
|
|
65
|
+
break if pos >= data.size
|
|
66
|
+
top_delta = data[pos].ord
|
|
67
|
+
break if top_delta == 0xFF
|
|
68
|
+
|
|
69
|
+
length = data[pos + 1].ord
|
|
70
|
+
# Skip padding byte, read pixels, skip end padding
|
|
71
|
+
pixel_data = data[pos + 3, length]
|
|
72
|
+
break unless pixel_data
|
|
73
|
+
|
|
74
|
+
pixel_data.bytes.each_with_index do |color, i|
|
|
75
|
+
y = top_delta + i
|
|
76
|
+
pixels[y] = color if y < height
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
pos += length + 4
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
pixels
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def load_status_bar
|
|
86
|
+
@status_bar = load_graphic('STBAR')
|
|
87
|
+
@arms_background = load_graphic('STARMS')
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def load_numbers
|
|
91
|
+
@numbers = {}
|
|
92
|
+
# Large red numbers for health/ammo
|
|
93
|
+
(0..9).each do |n|
|
|
94
|
+
@numbers[n] = load_graphic("STTNUM#{n}")
|
|
95
|
+
end
|
|
96
|
+
@numbers['-'] = load_graphic('STTMINUS')
|
|
97
|
+
@numbers['%'] = load_graphic('STTPRCNT')
|
|
98
|
+
|
|
99
|
+
# Small grey numbers for arms (weapon not owned)
|
|
100
|
+
@grey_numbers = {}
|
|
101
|
+
(0..9).each do |n|
|
|
102
|
+
@grey_numbers[n] = load_graphic("STGNUM#{n}")
|
|
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
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def load_weapons
|
|
113
|
+
@weapons = {}
|
|
114
|
+
|
|
115
|
+
# Pistol frames (PISG = pistol gun)
|
|
116
|
+
@weapons[:pistol] = {
|
|
117
|
+
idle: load_graphic('PISGA0'),
|
|
118
|
+
fire: [
|
|
119
|
+
load_graphic('PISGB0'),
|
|
120
|
+
load_graphic('PISGC0'),
|
|
121
|
+
load_graphic('PISGD0'),
|
|
122
|
+
load_graphic('PISGE0')
|
|
123
|
+
].compact,
|
|
124
|
+
flash: [
|
|
125
|
+
load_graphic('PISFA0'),
|
|
126
|
+
load_graphic('PISFB0')
|
|
127
|
+
].compact
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Fist frames (PUNG = punch)
|
|
131
|
+
@weapons[:fist] = {
|
|
132
|
+
idle: load_graphic('PUNGA0'),
|
|
133
|
+
fire: [
|
|
134
|
+
load_graphic('PUNGB0'),
|
|
135
|
+
load_graphic('PUNGC0'),
|
|
136
|
+
load_graphic('PUNGD0')
|
|
137
|
+
].compact
|
|
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
|
+
}
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def load_faces
|
|
203
|
+
@faces = {}
|
|
204
|
+
|
|
205
|
+
# Straight ahead faces at different health levels
|
|
206
|
+
# STF = status face, ST = straight, 0-4 = health level (4=full, 0=dying)
|
|
207
|
+
(0..4).each do |health_level|
|
|
208
|
+
@faces[health_level] = {
|
|
209
|
+
straight: [
|
|
210
|
+
load_graphic("STFST#{health_level}0"),
|
|
211
|
+
load_graphic("STFST#{health_level}1"),
|
|
212
|
+
load_graphic("STFST#{health_level}2")
|
|
213
|
+
].compact,
|
|
214
|
+
left: load_graphic("STFTL#{health_level}0"),
|
|
215
|
+
right: load_graphic("STFTR#{health_level}0"),
|
|
216
|
+
ouch: load_graphic("STFOUCH#{health_level}"),
|
|
217
|
+
evil: load_graphic("STFEVL#{health_level}"),
|
|
218
|
+
kill: load_graphic("STFKILL#{health_level}")
|
|
219
|
+
}
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Special faces
|
|
223
|
+
@faces[:dead] = load_graphic('STFDEAD0')
|
|
224
|
+
@faces[:god] = load_graphic('STFGOD0')
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def load_keys
|
|
228
|
+
@keys = {
|
|
229
|
+
blue_card: load_graphic('STKEYS0'),
|
|
230
|
+
yellow_card: load_graphic('STKEYS1'),
|
|
231
|
+
red_card: load_graphic('STKEYS2'),
|
|
232
|
+
blue_skull: load_graphic('STKEYS3'),
|
|
233
|
+
yellow_skull: load_graphic('STKEYS4'),
|
|
234
|
+
red_skull: load_graphic('STKEYS5')
|
|
235
|
+
}
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Simple sprite container for HUD graphics
|
|
240
|
+
class HudSprite
|
|
241
|
+
attr_reader :name, :width, :height, :left_offset, :top_offset, :columns
|
|
242
|
+
|
|
243
|
+
def initialize(name, width, height, left_offset, top_offset, columns)
|
|
244
|
+
@name = name
|
|
245
|
+
@width = width
|
|
246
|
+
@height = height
|
|
247
|
+
@left_offset = left_offset
|
|
248
|
+
@top_offset = top_offset
|
|
249
|
+
@columns = columns
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def column_pixels(x)
|
|
253
|
+
@columns[x % @width]
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|