doom 0.2.0 → 0.3.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/map/data.rb +280 -0
- data/lib/doom/platform/gosu_window.rb +237 -0
- data/lib/doom/render/renderer.rb +1218 -0
- data/lib/doom/version.rb +1 -1
- data/lib/doom/wad/colormap.rb +38 -0
- data/lib/doom/wad/flat.rb +61 -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 +163 -0
- data/lib/doom/wad_downloader.rb +143 -0
- data/lib/doom.rb +56 -37
- metadata +32 -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
data/lib/doom/version.rb
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
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 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
|
+
def self.load(wad)
|
|
26
|
+
data = wad.read_lump('COLORMAP')
|
|
27
|
+
raise Error, 'COLORMAP lump not found' unless data
|
|
28
|
+
|
|
29
|
+
maps = MAPS.times.map do |i|
|
|
30
|
+
offset = i * MAP_SIZE
|
|
31
|
+
data[offset, MAP_SIZE].bytes
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
new(maps)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
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 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
|
+
def self.load_all(wad)
|
|
51
|
+
entries = wad.lumps_between('F_START', 'F_END')
|
|
52
|
+
entries.map do |entry|
|
|
53
|
+
next if entry.size != SIZE
|
|
54
|
+
|
|
55
|
+
data = wad.read_lump_at(entry)
|
|
56
|
+
new(entry.name, data.bytes)
|
|
57
|
+
end.compact
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
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
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Wad
|
|
5
|
+
class Reader
|
|
6
|
+
IWAD = 'IWAD'
|
|
7
|
+
PWAD = 'PWAD'
|
|
8
|
+
|
|
9
|
+
DirectoryEntry = Struct.new(:offset, :size, :name)
|
|
10
|
+
|
|
11
|
+
attr_reader :type, :num_lumps, :directory
|
|
12
|
+
|
|
13
|
+
def initialize(path)
|
|
14
|
+
@file = File.open(path, 'rb')
|
|
15
|
+
read_header
|
|
16
|
+
read_directory
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def find_lump(name)
|
|
20
|
+
@directory.find { |entry| entry.name == name.upcase }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def read_lump(name)
|
|
24
|
+
return @lump_cache[name] if @lump_cache.key?(name)
|
|
25
|
+
|
|
26
|
+
entry = find_lump(name)
|
|
27
|
+
return nil unless entry
|
|
28
|
+
|
|
29
|
+
@file.seek(entry.offset)
|
|
30
|
+
data = @file.read(entry.size)
|
|
31
|
+
@lump_cache[name] = data
|
|
32
|
+
data
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def read_lump_at(entry)
|
|
36
|
+
@file.seek(entry.offset)
|
|
37
|
+
@file.read(entry.size)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def lumps_between(start_marker, end_marker)
|
|
41
|
+
start_idx = @directory.index { |e| e.name == start_marker }
|
|
42
|
+
end_idx = @directory.index { |e| e.name == end_marker }
|
|
43
|
+
return [] unless start_idx && end_idx
|
|
44
|
+
|
|
45
|
+
@directory[start_idx + 1...end_idx]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def close
|
|
49
|
+
@file.close
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def read_header
|
|
55
|
+
data = @file.read(12)
|
|
56
|
+
@type = data[0, 4]
|
|
57
|
+
@num_lumps = data[4, 4].unpack1('V')
|
|
58
|
+
@directory_offset = data[8, 4].unpack1('V')
|
|
59
|
+
@lump_cache = {}
|
|
60
|
+
|
|
61
|
+
unless [@type == IWAD, @type == PWAD].any?
|
|
62
|
+
raise Error, "Invalid WAD type: #{@type}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def read_directory
|
|
67
|
+
@file.seek(@directory_offset)
|
|
68
|
+
@directory = @num_lumps.times.map do
|
|
69
|
+
data = @file.read(16)
|
|
70
|
+
DirectoryEntry.new(
|
|
71
|
+
data[0, 4].unpack1('V'),
|
|
72
|
+
data[4, 4].unpack1('V'),
|
|
73
|
+
data[8, 8].delete("\x00").upcase
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Wad
|
|
5
|
+
class Sprite
|
|
6
|
+
attr_reader :name, :width, :height, :left_offset, :top_offset
|
|
7
|
+
|
|
8
|
+
def initialize(name, width, height, left_offset, top_offset, columns)
|
|
9
|
+
@name = name
|
|
10
|
+
@width = width
|
|
11
|
+
@height = height
|
|
12
|
+
@left_offset = left_offset
|
|
13
|
+
@top_offset = top_offset
|
|
14
|
+
@columns = columns
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def column_pixels(x)
|
|
18
|
+
@columns[x] || []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Load a sprite from a WAD patch lump
|
|
22
|
+
def self.load(wad, lump_name)
|
|
23
|
+
entry = wad.directory.find { |e| e.name == lump_name }
|
|
24
|
+
return nil unless entry
|
|
25
|
+
|
|
26
|
+
data = wad.read_lump_at(entry)
|
|
27
|
+
return nil if data.size < 8
|
|
28
|
+
|
|
29
|
+
width = data[0, 2].unpack1('v')
|
|
30
|
+
height = data[2, 2].unpack1('v')
|
|
31
|
+
left_offset = data[4, 2].unpack1('s<')
|
|
32
|
+
top_offset = data[6, 2].unpack1('s<')
|
|
33
|
+
|
|
34
|
+
# Read column offsets
|
|
35
|
+
column_offsets = []
|
|
36
|
+
width.times do |i|
|
|
37
|
+
column_offsets << data[8 + i * 4, 4].unpack1('V')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Read columns
|
|
41
|
+
columns = []
|
|
42
|
+
width.times do |x|
|
|
43
|
+
column = Array.new(height, nil) # nil = transparent
|
|
44
|
+
offset = column_offsets[x]
|
|
45
|
+
|
|
46
|
+
# Read posts for this column
|
|
47
|
+
loop do
|
|
48
|
+
break if offset >= data.size
|
|
49
|
+
row_start = data[offset].ord
|
|
50
|
+
break if row_start == 255 # End of column marker
|
|
51
|
+
|
|
52
|
+
post_height = data[offset + 1].ord
|
|
53
|
+
break if post_height == 0 || offset + 3 + post_height > data.size
|
|
54
|
+
|
|
55
|
+
# Skip padding byte, read pixels, skip trailing padding
|
|
56
|
+
post_height.times do |i|
|
|
57
|
+
y = row_start + i
|
|
58
|
+
if y < height
|
|
59
|
+
column[y] = data[offset + 3 + i].ord
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
offset += post_height + 4 # row_start + count + padding + pixels + padding
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
columns << column
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
new(lump_name, width, height, left_offset, top_offset, columns)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class SpriteManager
|
|
74
|
+
# Map thing types to sprite prefixes
|
|
75
|
+
THING_SPRITES = {
|
|
76
|
+
# Ammo
|
|
77
|
+
2007 => 'CLIP', # Clip
|
|
78
|
+
2048 => 'AMMO', # Box of ammo
|
|
79
|
+
2008 => 'SHEL', # Shells
|
|
80
|
+
2049 => 'SBOX', # Box of shells
|
|
81
|
+
2010 => 'ROCK', # Rocket
|
|
82
|
+
2046 => 'BROK', # Box of rockets
|
|
83
|
+
2047 => 'CELL', # Cell charge
|
|
84
|
+
17 => 'CELP', # Cell pack
|
|
85
|
+
|
|
86
|
+
# Weapons
|
|
87
|
+
2001 => 'SHOT', # Shotgun
|
|
88
|
+
2002 => 'MGUN', # Chaingun
|
|
89
|
+
2003 => 'LAUN', # Rocket launcher
|
|
90
|
+
2004 => 'PLAS', # Plasma rifle
|
|
91
|
+
2006 => 'BFUG', # BFG 9000
|
|
92
|
+
2005 => 'CSAW', # Chainsaw
|
|
93
|
+
|
|
94
|
+
# Health/Armor
|
|
95
|
+
2011 => 'STIM', # Stimpack
|
|
96
|
+
2012 => 'MEDI', # Medikit
|
|
97
|
+
2014 => 'BON1', # Health bonus
|
|
98
|
+
2015 => 'BON2', # Armor bonus
|
|
99
|
+
2018 => 'ARM1', # Green armor
|
|
100
|
+
2019 => 'ARM2', # Blue armor
|
|
101
|
+
|
|
102
|
+
# Keys
|
|
103
|
+
5 => 'BKEY', # Blue keycard
|
|
104
|
+
6 => 'YKEY', # Yellow keycard
|
|
105
|
+
13 => 'RKEY', # Red keycard
|
|
106
|
+
40 => 'BSKU', # Blue skull
|
|
107
|
+
39 => 'YSKU', # Yellow skull
|
|
108
|
+
38 => 'RSKU', # Red skull
|
|
109
|
+
|
|
110
|
+
# Decorations
|
|
111
|
+
2028 => 'COLU', # Light column
|
|
112
|
+
30 => 'COL1', # Tall green pillar
|
|
113
|
+
31 => 'COL2', # Short green pillar
|
|
114
|
+
32 => 'COL3', # Tall red pillar
|
|
115
|
+
33 => 'COL4', # Short red pillar
|
|
116
|
+
34 => 'CAND', # Candle
|
|
117
|
+
44 => 'TBLU', # Tall blue torch
|
|
118
|
+
45 => 'TGRN', # Tall green torch
|
|
119
|
+
46 => 'TRED', # Tall red torch
|
|
120
|
+
48 => 'ELEC', # Tall tech column
|
|
121
|
+
35 => 'CBRA', # Candelabra
|
|
122
|
+
|
|
123
|
+
# Barrels
|
|
124
|
+
2035 => 'BAR1', # Exploding barrel
|
|
125
|
+
|
|
126
|
+
# Monsters
|
|
127
|
+
3004 => 'POSS', # Zombieman
|
|
128
|
+
9 => 'SPOS', # Shotgun guy
|
|
129
|
+
3001 => 'TROO', # Imp
|
|
130
|
+
3002 => 'SARG', # Demon
|
|
131
|
+
58 => 'SARG', # Spectre (same as Demon)
|
|
132
|
+
3003 => 'BOSS', # Baron of Hell
|
|
133
|
+
3005 => 'HEAD', # Cacodemon
|
|
134
|
+
3006 => 'SKUL', # Lost soul
|
|
135
|
+
7 => 'SPID', # Spider Mastermind
|
|
136
|
+
16 => 'CYBR', # Cyberdemon
|
|
137
|
+
}.freeze
|
|
138
|
+
|
|
139
|
+
def initialize(wad)
|
|
140
|
+
@wad = wad
|
|
141
|
+
@cache = {}
|
|
142
|
+
@rotation_cache = {}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Get default sprite (rotation 0 or 1)
|
|
146
|
+
def [](thing_type)
|
|
147
|
+
return @cache[thing_type] if @cache.key?(thing_type)
|
|
148
|
+
|
|
149
|
+
prefix = THING_SPRITES[thing_type]
|
|
150
|
+
return nil unless prefix
|
|
151
|
+
|
|
152
|
+
# Try to find sprite with A0 (all angles) or A1 (front facing)
|
|
153
|
+
sprite = Sprite.load(@wad, "#{prefix}A0") ||
|
|
154
|
+
Sprite.load(@wad, "#{prefix}A1")
|
|
155
|
+
|
|
156
|
+
@cache[thing_type] = sprite
|
|
157
|
+
sprite
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Get sprite prefix for a thing type
|
|
161
|
+
def prefix_for(thing_type)
|
|
162
|
+
THING_SPRITES[thing_type]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Get sprite for specific rotation (1-8, or 0 for all angles)
|
|
166
|
+
# viewer_angle: angle from viewer to sprite in radians
|
|
167
|
+
# thing_angle: thing's facing angle in degrees
|
|
168
|
+
def get_rotated(thing_type, viewer_angle, thing_angle)
|
|
169
|
+
prefix = THING_SPRITES[thing_type]
|
|
170
|
+
return nil unless prefix
|
|
171
|
+
|
|
172
|
+
# Check cache for rotation 0 (all angles) sprite
|
|
173
|
+
cache_key = "#{prefix}A0"
|
|
174
|
+
if @rotation_cache.key?(cache_key)
|
|
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
|
|
181
|
+
|
|
182
|
+
# Calculate rotation frame (1-8)
|
|
183
|
+
# Doom rotations: 1=front, 2=front-right, 3=right, etc. (clockwise)
|
|
184
|
+
# The angle we need is: viewer's angle to sprite - sprite's facing angle
|
|
185
|
+
angle_diff = viewer_angle - (thing_angle * Math::PI / 180.0)
|
|
186
|
+
|
|
187
|
+
# Normalize to 0-2π
|
|
188
|
+
angle_diff = angle_diff % (2 * Math::PI)
|
|
189
|
+
angle_diff += 2 * Math::PI if angle_diff < 0
|
|
190
|
+
|
|
191
|
+
# Convert to rotation frame (1-8)
|
|
192
|
+
# Each rotation covers 45 degrees (π/4 radians)
|
|
193
|
+
# Add π/8 to center the ranges
|
|
194
|
+
rotation = ((angle_diff + Math::PI / 8) / (Math::PI / 4)).to_i % 8 + 1
|
|
195
|
+
|
|
196
|
+
cache_key = "#{prefix}A#{rotation}"
|
|
197
|
+
unless @rotation_cache.key?(cache_key)
|
|
198
|
+
@rotation_cache[cache_key] = Sprite.load(@wad, cache_key)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
@rotation_cache[cache_key] || @cache[thing_type]
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Wad
|
|
5
|
+
class Texture
|
|
6
|
+
PatchRef = Struct.new(:x_offset, :y_offset, :patch_index)
|
|
7
|
+
|
|
8
|
+
attr_reader :name, :width, :height, :patch_refs
|
|
9
|
+
|
|
10
|
+
def initialize(name, width, height, patch_refs)
|
|
11
|
+
@name = name
|
|
12
|
+
@width = width
|
|
13
|
+
@height = height
|
|
14
|
+
@patch_refs = patch_refs
|
|
15
|
+
@columns = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def columns
|
|
19
|
+
@columns ||= build_columns
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def build_columns
|
|
23
|
+
# Will be built when patches are composited
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.load_all(wad)
|
|
28
|
+
pnames = load_pnames(wad)
|
|
29
|
+
textures = {}
|
|
30
|
+
|
|
31
|
+
%w[TEXTURE1 TEXTURE2].each do |lump_name|
|
|
32
|
+
data = wad.read_lump(lump_name)
|
|
33
|
+
next unless data
|
|
34
|
+
|
|
35
|
+
parse_texture_lump(data).each do |tex|
|
|
36
|
+
textures[tex.name] = tex
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
{ textures: textures, pnames: pnames }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.load_pnames(wad)
|
|
44
|
+
data = wad.read_lump('PNAMES')
|
|
45
|
+
return [] unless data
|
|
46
|
+
|
|
47
|
+
count = data[0, 4].unpack1('V')
|
|
48
|
+
count.times.map do |i|
|
|
49
|
+
data[4 + i * 8, 8].delete("\x00").upcase
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.parse_texture_lump(data)
|
|
54
|
+
num_textures = data[0, 4].unpack1('V')
|
|
55
|
+
offsets = num_textures.times.map do |i|
|
|
56
|
+
data[4 + i * 4, 4].unpack1('V')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
offsets.map do |offset|
|
|
60
|
+
parse_texture(data, offset)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.parse_texture(data, offset)
|
|
65
|
+
name = data[offset, 8].delete("\x00").upcase
|
|
66
|
+
width = data[offset + 12, 2].unpack1('v')
|
|
67
|
+
height = data[offset + 14, 2].unpack1('v')
|
|
68
|
+
patch_count = data[offset + 20, 2].unpack1('v')
|
|
69
|
+
|
|
70
|
+
patch_refs = patch_count.times.map do |i|
|
|
71
|
+
po = offset + 22 + i * 10
|
|
72
|
+
PatchRef.new(
|
|
73
|
+
data[po, 2].unpack1('s<'),
|
|
74
|
+
data[po + 2, 2].unpack1('s<'),
|
|
75
|
+
data[po + 4, 2].unpack1('v')
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
new(name, width, height, patch_refs)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class TextureManager
|
|
84
|
+
attr_reader :textures, :pnames, :patches
|
|
85
|
+
|
|
86
|
+
def initialize(wad)
|
|
87
|
+
@wad = wad
|
|
88
|
+
result = Texture.load_all(wad)
|
|
89
|
+
@textures = result[:textures]
|
|
90
|
+
@pnames = result[:pnames]
|
|
91
|
+
@patches = {}
|
|
92
|
+
@composite_cache = {}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def [](name)
|
|
96
|
+
return nil if name.nil? || name.empty? || name == '-'
|
|
97
|
+
|
|
98
|
+
@composite_cache[name] ||= build_composite(name.upcase)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def get_patch(index)
|
|
102
|
+
name = @pnames[index]
|
|
103
|
+
return nil unless name
|
|
104
|
+
|
|
105
|
+
@patches[name] ||= Patch.load(@wad, name)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def build_composite(name)
|
|
111
|
+
texture = @textures[name]
|
|
112
|
+
return nil unless texture
|
|
113
|
+
|
|
114
|
+
columns = Array.new(texture.width) { [] }
|
|
115
|
+
|
|
116
|
+
texture.patch_refs.each do |pref|
|
|
117
|
+
patch = get_patch(pref.patch_index)
|
|
118
|
+
next unless patch
|
|
119
|
+
|
|
120
|
+
patch.columns.each_with_index do |posts, px|
|
|
121
|
+
tx = pref.x_offset + px
|
|
122
|
+
next if tx < 0 || tx >= texture.width
|
|
123
|
+
|
|
124
|
+
posts.each do |post|
|
|
125
|
+
columns[tx] << Patch::Post.new(
|
|
126
|
+
pref.y_offset + post.top_delta,
|
|
127
|
+
post.pixels
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
CompositeTexture.new(name, texture.width, texture.height, columns)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
class CompositeTexture
|
|
138
|
+
attr_reader :name, :width, :height, :columns
|
|
139
|
+
|
|
140
|
+
def initialize(name, width, height, columns)
|
|
141
|
+
@name = name
|
|
142
|
+
@width = width
|
|
143
|
+
@height = height
|
|
144
|
+
@columns = columns
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def column_pixels(x, height_needed = nil)
|
|
148
|
+
x = x % @width
|
|
149
|
+
posts = @columns[x]
|
|
150
|
+
height_needed ||= @height
|
|
151
|
+
|
|
152
|
+
pixels = Array.new(height_needed, 0)
|
|
153
|
+
posts.each do |post|
|
|
154
|
+
post.pixels.each_with_index do |color, i|
|
|
155
|
+
y = (post.top_delta + i) % @height
|
|
156
|
+
pixels[y] = color if y < height_needed
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
pixels
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|