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,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
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Map
|
|
5
|
+
Vertex = Struct.new(:x, :y)
|
|
6
|
+
|
|
7
|
+
Thing = Struct.new(:x, :y, :angle, :type, :flags)
|
|
8
|
+
|
|
9
|
+
Linedef = Struct.new(:v1, :v2, :flags, :special, :tag, :sidedef_right, :sidedef_left) do
|
|
10
|
+
FLAGS = {
|
|
11
|
+
BLOCKING: 0x0001,
|
|
12
|
+
BLOCKMONSTERS: 0x0002,
|
|
13
|
+
TWOSIDED: 0x0004,
|
|
14
|
+
DONTPEGTOP: 0x0008,
|
|
15
|
+
DONTPEGBOTTOM: 0x0010,
|
|
16
|
+
SECRET: 0x0020,
|
|
17
|
+
SOUNDBLOCK: 0x0040,
|
|
18
|
+
DONTDRAW: 0x0080,
|
|
19
|
+
MAPPED: 0x0100
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
def two_sided?
|
|
23
|
+
(flags & FLAGS[:TWOSIDED]) != 0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def upper_unpegged?
|
|
27
|
+
(flags & FLAGS[:DONTPEGTOP]) != 0
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def lower_unpegged?
|
|
31
|
+
(flags & FLAGS[:DONTPEGBOTTOM]) != 0
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Sidedef = Struct.new(:x_offset, :y_offset, :upper_texture, :lower_texture, :middle_texture, :sector)
|
|
36
|
+
|
|
37
|
+
Sector = Struct.new(:floor_height, :ceiling_height, :floor_texture, :ceiling_texture, :light_level, :special, :tag)
|
|
38
|
+
|
|
39
|
+
Seg = Struct.new(:v1, :v2, :angle, :linedef, :direction, :offset)
|
|
40
|
+
|
|
41
|
+
Subsector = Struct.new(:seg_count, :first_seg)
|
|
42
|
+
|
|
43
|
+
class Node
|
|
44
|
+
SUBSECTOR_FLAG = 0x8000
|
|
45
|
+
|
|
46
|
+
attr_reader :x, :y, :dx, :dy, :bbox_right, :bbox_left, :child_right, :child_left
|
|
47
|
+
|
|
48
|
+
BBox = Struct.new(:top, :bottom, :left, :right)
|
|
49
|
+
|
|
50
|
+
def initialize(x, y, dx, dy, bbox_right, bbox_left, child_right, child_left)
|
|
51
|
+
@x = x
|
|
52
|
+
@y = y
|
|
53
|
+
@dx = dx
|
|
54
|
+
@dy = dy
|
|
55
|
+
@bbox_right = bbox_right
|
|
56
|
+
@bbox_left = bbox_left
|
|
57
|
+
@child_right = child_right
|
|
58
|
+
@child_left = child_left
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def right_is_subsector?
|
|
62
|
+
(@child_right & SUBSECTOR_FLAG) != 0
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def left_is_subsector?
|
|
66
|
+
(@child_left & SUBSECTOR_FLAG) != 0
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def right_index
|
|
70
|
+
@child_right & ~SUBSECTOR_FLAG
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def left_index
|
|
74
|
+
@child_left & ~SUBSECTOR_FLAG
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class MapData
|
|
79
|
+
attr_reader :name, :things, :vertices, :linedefs, :sidedefs, :sectors, :segs, :subsectors, :nodes
|
|
80
|
+
|
|
81
|
+
def initialize(name)
|
|
82
|
+
@name = name
|
|
83
|
+
@things = []
|
|
84
|
+
@vertices = []
|
|
85
|
+
@linedefs = []
|
|
86
|
+
@sidedefs = []
|
|
87
|
+
@sectors = []
|
|
88
|
+
@segs = []
|
|
89
|
+
@subsectors = []
|
|
90
|
+
@nodes = []
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.load(wad, map_name)
|
|
94
|
+
map = new(map_name)
|
|
95
|
+
|
|
96
|
+
lump_idx = wad.directory.index { |e| e.name == map_name.upcase }
|
|
97
|
+
raise Error, "Map #{map_name} not found" unless lump_idx
|
|
98
|
+
|
|
99
|
+
map.load_things(wad.read_lump_at(wad.directory[lump_idx + 1]))
|
|
100
|
+
map.load_linedefs(wad.read_lump_at(wad.directory[lump_idx + 2]))
|
|
101
|
+
map.load_sidedefs(wad.read_lump_at(wad.directory[lump_idx + 3]))
|
|
102
|
+
map.load_vertices(wad.read_lump_at(wad.directory[lump_idx + 4]))
|
|
103
|
+
map.load_segs(wad.read_lump_at(wad.directory[lump_idx + 5]))
|
|
104
|
+
map.load_subsectors(wad.read_lump_at(wad.directory[lump_idx + 6]))
|
|
105
|
+
map.load_nodes(wad.read_lump_at(wad.directory[lump_idx + 7]))
|
|
106
|
+
map.load_sectors(wad.read_lump_at(wad.directory[lump_idx + 8]))
|
|
107
|
+
|
|
108
|
+
map
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def load_things(data)
|
|
112
|
+
count = data.size / 10
|
|
113
|
+
count.times do |i|
|
|
114
|
+
offset = i * 10
|
|
115
|
+
@things << Thing.new(
|
|
116
|
+
data[offset, 2].unpack1('s<'),
|
|
117
|
+
data[offset + 2, 2].unpack1('s<'),
|
|
118
|
+
data[offset + 4, 2].unpack1('v'),
|
|
119
|
+
data[offset + 6, 2].unpack1('v'),
|
|
120
|
+
data[offset + 8, 2].unpack1('v')
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def load_vertices(data)
|
|
126
|
+
count = data.size / 4
|
|
127
|
+
count.times do |i|
|
|
128
|
+
offset = i * 4
|
|
129
|
+
@vertices << Vertex.new(
|
|
130
|
+
data[offset, 2].unpack1('s<'),
|
|
131
|
+
data[offset + 2, 2].unpack1('s<')
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def load_linedefs(data)
|
|
137
|
+
count = data.size / 14
|
|
138
|
+
count.times do |i|
|
|
139
|
+
offset = i * 14
|
|
140
|
+
@linedefs << Linedef.new(
|
|
141
|
+
data[offset, 2].unpack1('v'),
|
|
142
|
+
data[offset + 2, 2].unpack1('v'),
|
|
143
|
+
data[offset + 4, 2].unpack1('v'),
|
|
144
|
+
data[offset + 6, 2].unpack1('v'),
|
|
145
|
+
data[offset + 8, 2].unpack1('v'),
|
|
146
|
+
data[offset + 10, 2].unpack1('s<'),
|
|
147
|
+
data[offset + 12, 2].unpack1('s<')
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def load_sidedefs(data)
|
|
153
|
+
count = data.size / 30
|
|
154
|
+
count.times do |i|
|
|
155
|
+
offset = i * 30
|
|
156
|
+
@sidedefs << Sidedef.new(
|
|
157
|
+
data[offset, 2].unpack1('s<'),
|
|
158
|
+
data[offset + 2, 2].unpack1('s<'),
|
|
159
|
+
data[offset + 4, 8].delete("\x00").strip,
|
|
160
|
+
data[offset + 12, 8].delete("\x00").strip,
|
|
161
|
+
data[offset + 20, 8].delete("\x00").strip,
|
|
162
|
+
data[offset + 28, 2].unpack1('v')
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def load_sectors(data)
|
|
168
|
+
count = data.size / 26
|
|
169
|
+
count.times do |i|
|
|
170
|
+
offset = i * 26
|
|
171
|
+
@sectors << Sector.new(
|
|
172
|
+
data[offset, 2].unpack1('s<'),
|
|
173
|
+
data[offset + 2, 2].unpack1('s<'),
|
|
174
|
+
data[offset + 4, 8].delete("\x00").strip,
|
|
175
|
+
data[offset + 12, 8].delete("\x00").strip,
|
|
176
|
+
data[offset + 20, 2].unpack1('v'),
|
|
177
|
+
data[offset + 22, 2].unpack1('v'),
|
|
178
|
+
data[offset + 24, 2].unpack1('v')
|
|
179
|
+
)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def load_segs(data)
|
|
184
|
+
count = data.size / 12
|
|
185
|
+
count.times do |i|
|
|
186
|
+
offset = i * 12
|
|
187
|
+
@segs << Seg.new(
|
|
188
|
+
data[offset, 2].unpack1('v'),
|
|
189
|
+
data[offset + 2, 2].unpack1('v'),
|
|
190
|
+
data[offset + 4, 2].unpack1('s<'),
|
|
191
|
+
data[offset + 6, 2].unpack1('v'),
|
|
192
|
+
data[offset + 8, 2].unpack1('v'),
|
|
193
|
+
data[offset + 10, 2].unpack1('s<')
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def load_subsectors(data)
|
|
199
|
+
count = data.size / 4
|
|
200
|
+
count.times do |i|
|
|
201
|
+
offset = i * 4
|
|
202
|
+
@subsectors << Subsector.new(
|
|
203
|
+
data[offset, 2].unpack1('v'),
|
|
204
|
+
data[offset + 2, 2].unpack1('v')
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def load_nodes(data)
|
|
210
|
+
count = data.size / 28
|
|
211
|
+
count.times do |i|
|
|
212
|
+
offset = i * 28
|
|
213
|
+
bbox_right = Node::BBox.new(
|
|
214
|
+
data[offset + 8, 2].unpack1('s<'),
|
|
215
|
+
data[offset + 10, 2].unpack1('s<'),
|
|
216
|
+
data[offset + 12, 2].unpack1('s<'),
|
|
217
|
+
data[offset + 14, 2].unpack1('s<')
|
|
218
|
+
)
|
|
219
|
+
bbox_left = Node::BBox.new(
|
|
220
|
+
data[offset + 16, 2].unpack1('s<'),
|
|
221
|
+
data[offset + 18, 2].unpack1('s<'),
|
|
222
|
+
data[offset + 20, 2].unpack1('s<'),
|
|
223
|
+
data[offset + 22, 2].unpack1('s<')
|
|
224
|
+
)
|
|
225
|
+
@nodes << Node.new(
|
|
226
|
+
data[offset, 2].unpack1('s<'),
|
|
227
|
+
data[offset + 2, 2].unpack1('s<'),
|
|
228
|
+
data[offset + 4, 2].unpack1('s<'),
|
|
229
|
+
data[offset + 6, 2].unpack1('s<'),
|
|
230
|
+
bbox_right,
|
|
231
|
+
bbox_left,
|
|
232
|
+
data[offset + 24, 2].unpack1('v'),
|
|
233
|
+
data[offset + 26, 2].unpack1('v')
|
|
234
|
+
)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def player_start
|
|
239
|
+
@things.find { |t| t.type == 1 }
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Find the sector at a given position by traversing the BSP tree
|
|
243
|
+
def sector_at(x, y)
|
|
244
|
+
subsector = subsector_at(x, y)
|
|
245
|
+
return nil unless subsector
|
|
246
|
+
|
|
247
|
+
# Get sector from first seg of subsector
|
|
248
|
+
seg = @segs[subsector.first_seg]
|
|
249
|
+
return nil unless seg
|
|
250
|
+
|
|
251
|
+
linedef = @linedefs[seg.linedef]
|
|
252
|
+
sidedef_idx = seg.direction == 0 ? linedef.sidedef_right : linedef.sidedef_left
|
|
253
|
+
return nil if sidedef_idx < 0
|
|
254
|
+
|
|
255
|
+
@sectors[@sidedefs[sidedef_idx].sector]
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Find the subsector containing a point
|
|
259
|
+
def subsector_at(x, y)
|
|
260
|
+
node_idx = @nodes.size - 1
|
|
261
|
+
while (node_idx & Node::SUBSECTOR_FLAG) == 0
|
|
262
|
+
node = @nodes[node_idx]
|
|
263
|
+
side = point_on_side(x, y, node)
|
|
264
|
+
node_idx = side == 0 ? node.child_right : node.child_left
|
|
265
|
+
end
|
|
266
|
+
@subsectors[node_idx & ~Node::SUBSECTOR_FLAG]
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
private
|
|
270
|
+
|
|
271
|
+
def point_on_side(x, y, node)
|
|
272
|
+
dx = x - node.x
|
|
273
|
+
dy = y - node.y
|
|
274
|
+
left = dy * node.dx
|
|
275
|
+
right = dx * node.dy
|
|
276
|
+
right >= left ? 0 : 1
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|