doom 0.2.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 +75 -115
- data/bin/doom +47 -58
- data/lib/doom/game/player_state.rb +221 -0
- data/lib/doom/game/sector_actions.rb +162 -0
- data/lib/doom/map/data.rb +280 -0
- data/lib/doom/platform/gosu_window.rb +414 -0
- data/lib/doom/render/renderer.rb +1272 -0
- 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 +32 -0
- data/lib/doom/wad/flat.rb +40 -0
- data/lib/doom/wad/hud_graphics.rb +189 -0
- data/lib/doom/wad/palette.rb +37 -0
- data/lib/doom/wad/patch.rb +61 -0
- data/lib/doom/wad/reader.rb +79 -0
- data/lib/doom/wad/sprite.rb +205 -0
- data/lib/doom/wad/texture.rb +153 -0
- data/lib/doom/wad_downloader.rb +143 -0
- data/lib/doom.rb +70 -37
- metadata +37 -35
- data/LICENSE.txt +0 -21
- data/bin/console +0 -15
- data/bin/setup +0 -8
- data/bin/wad +0 -152
- data/lib/doom/bsp_renderer.rb +0 -90
- data/lib/doom/game.rb +0 -84
- data/lib/doom/hud.rb +0 -80
- data/lib/doom/map_loader.rb +0 -255
- data/lib/doom/renderer.rb +0 -32
- data/lib/doom/sprite_loader.rb +0 -88
- data/lib/doom/sprite_renderer.rb +0 -56
- data/lib/doom/texture_loader.rb +0 -138
- data/lib/doom/texture_mapper.rb +0 -57
- data/lib/doom/wad_loader.rb +0 -106
- data/lib/doom/window.rb +0 -41
|
@@ -0,0 +1,166 @@
|
|
|
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
|
+
# X positions are RIGHT EDGE of each number area (numbers are right-aligned)
|
|
12
|
+
# Y positions relative to status bar top
|
|
13
|
+
|
|
14
|
+
# Right edge X positions for numbers
|
|
15
|
+
AMMO_RIGHT_X = 44 # ST_AMMOX - right edge of 3-digit ammo
|
|
16
|
+
HEALTH_RIGHT_X = 90 # ST_HEALTHX - right edge of 3-digit health
|
|
17
|
+
ARMOR_RIGHT_X = 221 # ST_ARMORX - right edge of 3-digit armor
|
|
18
|
+
|
|
19
|
+
FACE_X = 149 # Adjusted for proper centering in face background
|
|
20
|
+
KEYS_X = 239 # ST_KEY0X
|
|
21
|
+
|
|
22
|
+
NUM_WIDTH = 14 # Width of each digit
|
|
23
|
+
|
|
24
|
+
def initialize(hud_graphics, player_state)
|
|
25
|
+
@gfx = hud_graphics
|
|
26
|
+
@player = player_state
|
|
27
|
+
@face_timer = 0
|
|
28
|
+
@face_index = 0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def render(framebuffer)
|
|
32
|
+
# Draw status bar background
|
|
33
|
+
draw_sprite(framebuffer, @gfx.status_bar, 0, STATUS_BAR_Y) if @gfx.status_bar
|
|
34
|
+
|
|
35
|
+
# Y position for numbers (3 pixels from top of status bar)
|
|
36
|
+
num_y = STATUS_BAR_Y + 3
|
|
37
|
+
|
|
38
|
+
# Draw ammo count (right-aligned ending at AMMO_RIGHT_X)
|
|
39
|
+
draw_number_right(framebuffer, @player.current_ammo, AMMO_RIGHT_X, num_y) if @player.current_ammo
|
|
40
|
+
|
|
41
|
+
# Draw health with percent (right-aligned ending at HEALTH_RIGHT_X)
|
|
42
|
+
draw_number_right(framebuffer, @player.health, HEALTH_RIGHT_X, num_y)
|
|
43
|
+
draw_percent(framebuffer, HEALTH_RIGHT_X, num_y)
|
|
44
|
+
|
|
45
|
+
# Draw face
|
|
46
|
+
draw_face(framebuffer)
|
|
47
|
+
|
|
48
|
+
# Draw armor with percent (right-aligned ending at ARMOR_RIGHT_X)
|
|
49
|
+
draw_number_right(framebuffer, @player.armor, ARMOR_RIGHT_X, num_y)
|
|
50
|
+
draw_percent(framebuffer, ARMOR_RIGHT_X, num_y)
|
|
51
|
+
|
|
52
|
+
# Draw keys
|
|
53
|
+
draw_keys(framebuffer)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def update
|
|
57
|
+
# Cycle face animation
|
|
58
|
+
@face_timer += 1
|
|
59
|
+
if @face_timer > 15 # Change face every ~0.5 seconds
|
|
60
|
+
@face_timer = 0
|
|
61
|
+
@face_index = (@face_index + 1) % 3
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def draw_sprite(framebuffer, sprite, x, y)
|
|
68
|
+
return unless sprite
|
|
69
|
+
|
|
70
|
+
sprite.width.times do |sx|
|
|
71
|
+
column = sprite.column_pixels(sx)
|
|
72
|
+
next unless column
|
|
73
|
+
|
|
74
|
+
draw_x = x + sx
|
|
75
|
+
next if draw_x < 0 || draw_x >= SCREEN_WIDTH
|
|
76
|
+
|
|
77
|
+
column.each_with_index do |color, sy|
|
|
78
|
+
next unless color
|
|
79
|
+
|
|
80
|
+
draw_y = y + sy
|
|
81
|
+
next if draw_y < 0 || draw_y >= SCREEN_HEIGHT
|
|
82
|
+
|
|
83
|
+
framebuffer[draw_y * SCREEN_WIDTH + draw_x] = color
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Draw number right-aligned with right edge at right_x
|
|
89
|
+
def draw_number_right(framebuffer, value, right_x, y)
|
|
90
|
+
return unless value
|
|
91
|
+
|
|
92
|
+
value = value.to_i.clamp(-999, 999)
|
|
93
|
+
str = value.to_s
|
|
94
|
+
|
|
95
|
+
# Draw from right to left, starting from right edge
|
|
96
|
+
current_x = right_x
|
|
97
|
+
str.reverse.each_char do |char|
|
|
98
|
+
digit_sprite = if char == '-'
|
|
99
|
+
@gfx.numbers['-']
|
|
100
|
+
else
|
|
101
|
+
@gfx.numbers[char.to_i]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
if digit_sprite
|
|
105
|
+
current_x -= NUM_WIDTH
|
|
106
|
+
draw_sprite(framebuffer, digit_sprite, current_x, y)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def draw_percent(framebuffer, x, y)
|
|
112
|
+
percent = @gfx.numbers['%']
|
|
113
|
+
draw_sprite(framebuffer, percent, x, y) if percent
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def draw_face(framebuffer)
|
|
117
|
+
# In DOOM, face sprite health levels are inverted: 0 = full health, 4 = dying
|
|
118
|
+
sprite_health = 4 - @player.health_level
|
|
119
|
+
faces = @gfx.faces[sprite_health]
|
|
120
|
+
return unless faces
|
|
121
|
+
|
|
122
|
+
# Get current face sprite
|
|
123
|
+
face = if @player.health <= 0
|
|
124
|
+
@gfx.faces[:dead]
|
|
125
|
+
elsif faces[:straight] && faces[:straight][@face_index]
|
|
126
|
+
faces[:straight][@face_index]
|
|
127
|
+
else
|
|
128
|
+
faces[:straight]&.first
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
return unless face
|
|
132
|
+
|
|
133
|
+
# Position face in the background area
|
|
134
|
+
face_x = FACE_X
|
|
135
|
+
face_y = STATUS_BAR_Y + 2 # Slightly below top of status bar
|
|
136
|
+
draw_sprite(framebuffer, face, face_x, face_y)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def draw_keys(framebuffer)
|
|
140
|
+
key_x = KEYS_X
|
|
141
|
+
key_spacing = 10
|
|
142
|
+
|
|
143
|
+
# Blue keys (top row)
|
|
144
|
+
if @player.keys[:blue_card]
|
|
145
|
+
draw_sprite(framebuffer, @gfx.keys[:blue_card], key_x, STATUS_BAR_Y + 3)
|
|
146
|
+
elsif @player.keys[:blue_skull]
|
|
147
|
+
draw_sprite(framebuffer, @gfx.keys[:blue_skull], key_x, STATUS_BAR_Y + 3)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Yellow keys (middle row)
|
|
151
|
+
if @player.keys[:yellow_card]
|
|
152
|
+
draw_sprite(framebuffer, @gfx.keys[:yellow_card], key_x, STATUS_BAR_Y + 13)
|
|
153
|
+
elsif @player.keys[:yellow_skull]
|
|
154
|
+
draw_sprite(framebuffer, @gfx.keys[:yellow_skull], key_x, STATUS_BAR_Y + 13)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Red keys (bottom row)
|
|
158
|
+
if @player.keys[:red_card]
|
|
159
|
+
draw_sprite(framebuffer, @gfx.keys[:red_card], key_x, STATUS_BAR_Y + 23)
|
|
160
|
+
elsif @player.keys[:red_skull]
|
|
161
|
+
draw_sprite(framebuffer, @gfx.keys[:red_skull], key_x, STATUS_BAR_Y + 23)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
def initialize(hud_graphics, player_state)
|
|
11
|
+
@gfx = hud_graphics
|
|
12
|
+
@player = player_state
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def render(framebuffer)
|
|
16
|
+
weapon_name = @player.weapon_name
|
|
17
|
+
weapon_data = @gfx.weapons[weapon_name]
|
|
18
|
+
return unless weapon_data
|
|
19
|
+
|
|
20
|
+
# Get the appropriate frame
|
|
21
|
+
sprite = if @player.attacking && weapon_data[:fire]
|
|
22
|
+
frame = @player.attack_frame.clamp(0, weapon_data[:fire].length - 1)
|
|
23
|
+
weapon_data[:fire][frame]
|
|
24
|
+
else
|
|
25
|
+
weapon_data[:idle]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
return unless sprite
|
|
29
|
+
|
|
30
|
+
# Calculate position with bob offset
|
|
31
|
+
bob_x = @player.weapon_bob_x.to_i
|
|
32
|
+
bob_y = @player.weapon_bob_y.to_i
|
|
33
|
+
|
|
34
|
+
# Center weapon horizontally (sprite width / 2 from center)
|
|
35
|
+
# Position weapon at bottom of view area
|
|
36
|
+
x = (SCREEN_WIDTH / 2) - (sprite.width / 2) + bob_x
|
|
37
|
+
# Only allow downward bob (positive values) to keep weapon bottom at status bar
|
|
38
|
+
clamped_bob_y = [bob_y, 0].max
|
|
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
|
|
51
|
+
|
|
52
|
+
draw_weapon_sprite(framebuffer, sprite, x, y)
|
|
53
|
+
|
|
54
|
+
# Draw muzzle flash for pistol
|
|
55
|
+
if @player.attacking && @player.attack_frame < 2
|
|
56
|
+
draw_muzzle_flash(framebuffer, weapon_name, x, y)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def draw_weapon_sprite(framebuffer, sprite, base_x, base_y)
|
|
63
|
+
return unless sprite
|
|
64
|
+
|
|
65
|
+
# Clip to screen bounds (don't draw over status bar)
|
|
66
|
+
max_y = WEAPON_AREA_HEIGHT - 1
|
|
67
|
+
|
|
68
|
+
sprite.width.times do |sx|
|
|
69
|
+
column = sprite.column_pixels(sx)
|
|
70
|
+
next unless column
|
|
71
|
+
|
|
72
|
+
draw_x = base_x + sx
|
|
73
|
+
next if draw_x < 0 || draw_x >= SCREEN_WIDTH
|
|
74
|
+
|
|
75
|
+
column.each_with_index do |color, sy|
|
|
76
|
+
next unless color # Skip transparent pixels
|
|
77
|
+
|
|
78
|
+
draw_y = base_y + sy
|
|
79
|
+
next if draw_y < 0 || draw_y > max_y
|
|
80
|
+
|
|
81
|
+
framebuffer[draw_y * SCREEN_WIDTH + draw_x] = color
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def draw_muzzle_flash(framebuffer, weapon_name, weapon_x, weapon_y)
|
|
87
|
+
weapon_data = @gfx.weapons[weapon_name]
|
|
88
|
+
return unless weapon_data && weapon_data[:flash]
|
|
89
|
+
|
|
90
|
+
flash_frame = @player.attack_frame.clamp(0, weapon_data[:flash].length - 1)
|
|
91
|
+
flash_sprite = weapon_data[:flash][flash_frame]
|
|
92
|
+
return unless flash_sprite
|
|
93
|
+
|
|
94
|
+
# Flash is drawn at weapon position (sprite handles offset)
|
|
95
|
+
flash_x = (SCREEN_WIDTH / 2) - flash_sprite.left_offset
|
|
96
|
+
flash_y = WEAPON_AREA_HEIGHT - flash_sprite.top_offset
|
|
97
|
+
|
|
98
|
+
draw_weapon_sprite(framebuffer, flash_sprite, flash_x, flash_y)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
data/lib/doom/version.rb
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Wad
|
|
5
|
+
class Colormap
|
|
6
|
+
MAPS = 34
|
|
7
|
+
MAP_SIZE = 256
|
|
8
|
+
|
|
9
|
+
attr_reader :maps
|
|
10
|
+
|
|
11
|
+
def initialize(maps)
|
|
12
|
+
@maps = maps
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def [](map_index)
|
|
16
|
+
@maps[map_index]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.load(wad)
|
|
20
|
+
data = wad.read_lump('COLORMAP')
|
|
21
|
+
raise Error, 'COLORMAP lump not found' unless data
|
|
22
|
+
|
|
23
|
+
maps = MAPS.times.map do |i|
|
|
24
|
+
offset = i * MAP_SIZE
|
|
25
|
+
data[offset, MAP_SIZE].bytes
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
new(maps)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Wad
|
|
5
|
+
class Flat
|
|
6
|
+
WIDTH = 64
|
|
7
|
+
HEIGHT = 64
|
|
8
|
+
SIZE = WIDTH * HEIGHT
|
|
9
|
+
|
|
10
|
+
attr_reader :name, :pixels
|
|
11
|
+
|
|
12
|
+
def width
|
|
13
|
+
WIDTH
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def height
|
|
17
|
+
HEIGHT
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(name, pixels)
|
|
21
|
+
@name = name
|
|
22
|
+
@pixels = pixels
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def [](x, y)
|
|
26
|
+
@pixels[(y & 63) * WIDTH + (x & 63)]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.load_all(wad)
|
|
30
|
+
entries = wad.lumps_between('F_START', 'F_END')
|
|
31
|
+
entries.map do |entry|
|
|
32
|
+
next if entry.size != SIZE
|
|
33
|
+
|
|
34
|
+
data = wad.read_lump_at(entry)
|
|
35
|
+
new(entry.name, data.bytes)
|
|
36
|
+
end.compact
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,189 @@
|
|
|
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, :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
|
+
end
|
|
88
|
+
|
|
89
|
+
def load_numbers
|
|
90
|
+
@numbers = {}
|
|
91
|
+
# Large red numbers for health/ammo
|
|
92
|
+
(0..9).each do |n|
|
|
93
|
+
@numbers[n] = load_graphic("STTNUM#{n}")
|
|
94
|
+
end
|
|
95
|
+
@numbers['-'] = load_graphic('STTMINUS')
|
|
96
|
+
@numbers['%'] = load_graphic('STTPRCNT')
|
|
97
|
+
|
|
98
|
+
# Small grey numbers for arms
|
|
99
|
+
@grey_numbers = {}
|
|
100
|
+
(0..9).each do |n|
|
|
101
|
+
@grey_numbers[n] = load_graphic("STGNUM#{n}")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def load_weapons
|
|
106
|
+
@weapons = {}
|
|
107
|
+
|
|
108
|
+
# Pistol frames (PISG = pistol gun)
|
|
109
|
+
@weapons[:pistol] = {
|
|
110
|
+
idle: load_graphic('PISGA0'),
|
|
111
|
+
fire: [
|
|
112
|
+
load_graphic('PISGB0'),
|
|
113
|
+
load_graphic('PISGC0'),
|
|
114
|
+
load_graphic('PISGD0'),
|
|
115
|
+
load_graphic('PISGE0')
|
|
116
|
+
].compact,
|
|
117
|
+
flash: [
|
|
118
|
+
load_graphic('PISFA0'),
|
|
119
|
+
load_graphic('PISFB0')
|
|
120
|
+
].compact
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Fist frames (PUNG = punch)
|
|
124
|
+
@weapons[:fist] = {
|
|
125
|
+
idle: load_graphic('PUNGA0'),
|
|
126
|
+
fire: [
|
|
127
|
+
load_graphic('PUNGB0'),
|
|
128
|
+
load_graphic('PUNGC0'),
|
|
129
|
+
load_graphic('PUNGD0')
|
|
130
|
+
].compact
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def load_faces
|
|
135
|
+
@faces = {}
|
|
136
|
+
|
|
137
|
+
# Straight ahead faces at different health levels
|
|
138
|
+
# STF = status face, ST = straight, 0-4 = health level (4=full, 0=dying)
|
|
139
|
+
(0..4).each do |health_level|
|
|
140
|
+
@faces[health_level] = {
|
|
141
|
+
straight: [
|
|
142
|
+
load_graphic("STFST#{health_level}0"),
|
|
143
|
+
load_graphic("STFST#{health_level}1"),
|
|
144
|
+
load_graphic("STFST#{health_level}2")
|
|
145
|
+
].compact,
|
|
146
|
+
left: load_graphic("STFTL#{health_level}0"),
|
|
147
|
+
right: load_graphic("STFTR#{health_level}0"),
|
|
148
|
+
ouch: load_graphic("STFOUCH#{health_level}"),
|
|
149
|
+
evil: load_graphic("STFEVL#{health_level}"),
|
|
150
|
+
kill: load_graphic("STFKILL#{health_level}")
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Special faces
|
|
155
|
+
@faces[:dead] = load_graphic('STFDEAD0')
|
|
156
|
+
@faces[:god] = load_graphic('STFGOD0')
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def load_keys
|
|
160
|
+
@keys = {
|
|
161
|
+
blue_card: load_graphic('STKEYS0'),
|
|
162
|
+
yellow_card: load_graphic('STKEYS1'),
|
|
163
|
+
red_card: load_graphic('STKEYS2'),
|
|
164
|
+
blue_skull: load_graphic('STKEYS3'),
|
|
165
|
+
yellow_skull: load_graphic('STKEYS4'),
|
|
166
|
+
red_skull: load_graphic('STKEYS5')
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Simple sprite container for HUD graphics
|
|
172
|
+
class HudSprite
|
|
173
|
+
attr_reader :name, :width, :height, :left_offset, :top_offset, :columns
|
|
174
|
+
|
|
175
|
+
def initialize(name, width, height, left_offset, top_offset, columns)
|
|
176
|
+
@name = name
|
|
177
|
+
@width = width
|
|
178
|
+
@height = height
|
|
179
|
+
@left_offset = left_offset
|
|
180
|
+
@top_offset = top_offset
|
|
181
|
+
@columns = columns
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def column_pixels(x)
|
|
185
|
+
@columns[x % @width]
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Wad
|
|
5
|
+
class Palette
|
|
6
|
+
COLORS = 256
|
|
7
|
+
PALETTES = 14
|
|
8
|
+
PALETTE_SIZE = COLORS * 3
|
|
9
|
+
|
|
10
|
+
attr_reader :colors
|
|
11
|
+
|
|
12
|
+
def initialize(colors)
|
|
13
|
+
@colors = colors
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def [](index)
|
|
17
|
+
@colors[index]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.load(wad, palette_index = 0)
|
|
21
|
+
data = wad.read_lump('PLAYPAL')
|
|
22
|
+
raise Error, 'PLAYPAL lump not found' unless data
|
|
23
|
+
|
|
24
|
+
offset = palette_index * PALETTE_SIZE
|
|
25
|
+
colors = COLORS.times.map do |i|
|
|
26
|
+
[
|
|
27
|
+
data[offset + i * 3].ord,
|
|
28
|
+
data[offset + i * 3 + 1].ord,
|
|
29
|
+
data[offset + i * 3 + 2].ord
|
|
30
|
+
]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
new(colors)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Wad
|
|
5
|
+
class Patch
|
|
6
|
+
Post = Struct.new(:top_delta, :pixels)
|
|
7
|
+
|
|
8
|
+
attr_reader :name, :width, :height, :left_offset, :top_offset, :columns
|
|
9
|
+
|
|
10
|
+
def initialize(name, width, height, left_offset, top_offset, columns)
|
|
11
|
+
@name = name
|
|
12
|
+
@width = width
|
|
13
|
+
@height = height
|
|
14
|
+
@left_offset = left_offset
|
|
15
|
+
@top_offset = top_offset
|
|
16
|
+
@columns = columns
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.load(wad, name)
|
|
20
|
+
data = wad.read_lump(name)
|
|
21
|
+
return nil unless data
|
|
22
|
+
|
|
23
|
+
parse(name, data)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.parse(name, data)
|
|
27
|
+
width = data[0, 2].unpack1('v')
|
|
28
|
+
height = data[2, 2].unpack1('v')
|
|
29
|
+
left_offset = data[4, 2].unpack1('s<')
|
|
30
|
+
top_offset = data[6, 2].unpack1('s<')
|
|
31
|
+
|
|
32
|
+
column_offsets = width.times.map do |i|
|
|
33
|
+
data[8 + i * 4, 4].unpack1('V')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
columns = column_offsets.map do |offset|
|
|
37
|
+
read_column(data, offset)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
new(name, width, height, left_offset, top_offset, columns)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.read_column(data, offset)
|
|
44
|
+
posts = []
|
|
45
|
+
pos = offset
|
|
46
|
+
|
|
47
|
+
loop do
|
|
48
|
+
top_delta = data[pos].ord
|
|
49
|
+
break if top_delta == 0xFF
|
|
50
|
+
|
|
51
|
+
length = data[pos + 1].ord
|
|
52
|
+
pixels = data[pos + 3, length].bytes
|
|
53
|
+
posts << Post.new(top_delta, pixels)
|
|
54
|
+
pos += length + 4
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
posts
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|