doom 0.6.0 → 0.11.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/LICENSE +17 -0
- data/README.md +65 -4
- data/bin/doom +138 -25
- data/lib/doom/benchmark.rb +282 -0
- data/lib/doom/game/animations.rb +9 -2
- data/lib/doom/game/combat.rb +377 -105
- data/lib/doom/game/framebuffer_blitter.rb +38 -0
- data/lib/doom/game/geometry.rb +68 -0
- data/lib/doom/game/intermission.rb +231 -0
- data/lib/doom/game/item_pickup.rb +108 -60
- data/lib/doom/game/menu.rb +347 -0
- data/lib/doom/game/monster_ai.rb +304 -91
- data/lib/doom/game/player.rb +105 -0
- data/lib/doom/game/player_physics.rb +384 -0
- data/lib/doom/game/player_state.rb +47 -64
- data/lib/doom/game/random.rb +82 -0
- data/lib/doom/game/sector_actions.rb +466 -29
- data/lib/doom/game/sector_effects.rb +25 -18
- data/lib/doom/game/snapshot.rb +584 -0
- data/lib/doom/game/sound_engine.rb +176 -0
- data/lib/doom/game/state_hash.rb +125 -0
- data/lib/doom/game/ticcmd.rb +61 -0
- data/lib/doom/game/world.rb +420 -0
- data/lib/doom/map/data.rb +95 -0
- data/lib/doom/net/client.rb +204 -0
- data/lib/doom/net/desync_monitor.rb +101 -0
- data/lib/doom/net/game_server.rb +232 -0
- data/lib/doom/net/lockstep.rb +170 -0
- data/lib/doom/net/protocol.rb +350 -0
- data/lib/doom/net/session.rb +282 -0
- data/lib/doom/net/transport.rb +110 -0
- data/lib/doom/platform/gosu_window.rb +820 -640
- data/lib/doom/platform/sdl.rb +74 -0
- data/lib/doom/platform/window_logic.rb +61 -0
- data/lib/doom/render/font.rb +80 -0
- data/lib/doom/render/hardware_renderer.rb +547 -0
- data/lib/doom/render/ray_tracing/bvh.rb +103 -0
- data/lib/doom/render/ray_tracing/material_state.rb +66 -0
- data/lib/doom/render/ray_tracing/texture_atlas.rb +78 -0
- data/lib/doom/render/ray_tracing_renderer.rb +940 -0
- data/lib/doom/render/renderer.rb +684 -330
- data/lib/doom/render/renderer_factory.rb +50 -0
- data/lib/doom/render/screen_melt.rb +71 -0
- data/lib/doom/render/spinel_native/kernel.rb +780 -0
- data/lib/doom/render/spinel_native_renderer.rb +162 -0
- data/lib/doom/render/status_bar.rb +14 -10
- data/lib/doom/render/weapon_renderer.rb +15 -17
- data/lib/doom/render/world_mesh.rb +270 -0
- data/lib/doom/render/zbuffer_renderer.rb +151 -0
- data/lib/doom/version.rb +1 -1
- data/lib/doom/wad/flat.rb +1 -1
- data/lib/doom/wad/hud_graphics.rb +7 -3
- data/lib/doom/wad/palette.rb +3 -3
- data/lib/doom/wad/patch.rb +1 -1
- data/lib/doom/wad/reader.rb +59 -10
- data/lib/doom/wad/sound.rb +93 -0
- data/lib/doom/wad/sprite.rb +60 -34
- data/lib/doom/wad/texture.rb +5 -5
- data/lib/doom/wad_downloader.rb +38 -56
- data/lib/doom.rb +235 -12
- metadata +100 -7
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Game
|
|
5
|
+
# Shared framebuffer blitting for the full-screen menu and intermission
|
|
6
|
+
# screens. Both draw palette-index sprites into a flat 320x240 framebuffer,
|
|
7
|
+
# so the clipping loop lived duplicated in each. It lives here once and uses
|
|
8
|
+
# the renderer's screen constants instead of hardcoded 320/240.
|
|
9
|
+
#
|
|
10
|
+
# A "sprite" here is anything exposing #width and #column_pixels(x), where a
|
|
11
|
+
# column is an array of palette indices (nil = transparent).
|
|
12
|
+
module FramebufferBlitter
|
|
13
|
+
# Blit a sprite's opaque pixels at (x, y), clipping to the screen.
|
|
14
|
+
def blit_sprite(framebuffer, sprite, x, y)
|
|
15
|
+
return unless sprite
|
|
16
|
+
|
|
17
|
+
w = Render::SCREEN_WIDTH
|
|
18
|
+
h = Render::SCREEN_HEIGHT
|
|
19
|
+
sprite.width.times do |col_x|
|
|
20
|
+
sx = x + col_x
|
|
21
|
+
next if sx < 0 || sx >= w
|
|
22
|
+
|
|
23
|
+
col = sprite.column_pixels(col_x)
|
|
24
|
+
next unless col
|
|
25
|
+
|
|
26
|
+
col.each_with_index do |color, col_y|
|
|
27
|
+
next unless color
|
|
28
|
+
|
|
29
|
+
sy = y + col_y
|
|
30
|
+
next if sy < 0 || sy >= h
|
|
31
|
+
|
|
32
|
+
framebuffer[(sy * w) + sx] = color
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Game
|
|
5
|
+
# Shared 2D geometry for the simulation. These three helpers were copy-pasted
|
|
6
|
+
# across combat, monster AI, player physics, world and sector actions (the
|
|
7
|
+
# last under the name point_line_dist); consolidated here so line-of-sight,
|
|
8
|
+
# collision and use-line checks share one definition.
|
|
9
|
+
#
|
|
10
|
+
# Pure functions of their arguments -- no state, no RNG, no wall clock -- so
|
|
11
|
+
# they stay deterministic and every peer computes the same result.
|
|
12
|
+
module Geometry
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# Do the open segments A (ax1,ay1)->(ax2,ay2) and B (bx1,by1)->(bx2,by2)
|
|
16
|
+
# cross? Used for line-of-sight and hitscan against walls. Parallel or
|
|
17
|
+
# barely-crossing segments (tiny determinant) count as no intersection.
|
|
18
|
+
def segments_intersect?(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2)
|
|
19
|
+
d1x = ax2 - ax1
|
|
20
|
+
d1y = ay2 - ay1
|
|
21
|
+
d2x = bx2 - bx1
|
|
22
|
+
d2y = by2 - by1
|
|
23
|
+
denom = (d1x * d2y) - (d1y * d2x)
|
|
24
|
+
return false if denom.abs < 0.001
|
|
25
|
+
|
|
26
|
+
dx = bx1 - ax1
|
|
27
|
+
dy = by1 - ay1
|
|
28
|
+
t = ((dx * d2y) - (dy * d2x)).to_f / denom
|
|
29
|
+
u = ((dx * d1y) - (dy * d1x)).to_f / denom
|
|
30
|
+
t > 0.0 && t < 1.0 && u >= 0.0 && u <= 1.0
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Does the segment (x1,y1)->(x2,y2) pass within `radius` of the circle
|
|
34
|
+
# centred at (cx,cy)? The collision test for a body of that radius sliding
|
|
35
|
+
# against a wall.
|
|
36
|
+
def line_circle_intersect?(x1, y1, x2, y2, cx, cy, radius)
|
|
37
|
+
dx = cx - x1
|
|
38
|
+
dy = cy - y1
|
|
39
|
+
line_dx = x2 - x1
|
|
40
|
+
line_dy = y2 - y1
|
|
41
|
+
line_len_sq = (line_dx * line_dx) + (line_dy * line_dy)
|
|
42
|
+
return false if line_len_sq.zero?
|
|
43
|
+
|
|
44
|
+
t = (((dx * line_dx) + (dy * line_dy)).to_f / line_len_sq).clamp(0.0, 1.0)
|
|
45
|
+
closest_x = x1 + (t * line_dx)
|
|
46
|
+
closest_y = y1 + (t * line_dy)
|
|
47
|
+
dist_sq = ((cx - closest_x)**2) + ((cy - closest_y)**2)
|
|
48
|
+
dist_sq < radius * radius
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Shortest distance from point (px,py) to the segment (x1,y1)->(x2,y2),
|
|
52
|
+
# clamped to the segment ends. Used for use-line and trigger proximity.
|
|
53
|
+
def point_to_segment_distance(px, py, x1, y1, x2, y2)
|
|
54
|
+
dx = px - x1
|
|
55
|
+
dy = py - y1
|
|
56
|
+
line_dx = x2 - x1
|
|
57
|
+
line_dy = y2 - y1
|
|
58
|
+
line_len_sq = (line_dx * line_dx) + (line_dy * line_dy)
|
|
59
|
+
return Math.sqrt((dx * dx) + (dy * dy)) if line_len_sq.zero?
|
|
60
|
+
|
|
61
|
+
t = (((dx * line_dx) + (dy * line_dy)).to_f / line_len_sq).clamp(0.0, 1.0)
|
|
62
|
+
closest_x = x1 + (t * line_dx)
|
|
63
|
+
closest_y = y1 + (t * line_dy)
|
|
64
|
+
Math.sqrt(((px - closest_x)**2) + ((py - closest_y)**2))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'framebuffer_blitter'
|
|
4
|
+
|
|
5
|
+
module Doom
|
|
6
|
+
module Game
|
|
7
|
+
# Intermission screen shown between levels.
|
|
8
|
+
# Displays kill%, item%, secret%, time, and par time.
|
|
9
|
+
class Intermission
|
|
10
|
+
include FramebufferBlitter
|
|
11
|
+
|
|
12
|
+
# Episode 1 par times in seconds (from Chocolate Doom)
|
|
13
|
+
PAR_TIMES = {
|
|
14
|
+
'E1M1' => 30, 'E1M2' => 75, 'E1M3' => 120, 'E1M4' => 90,
|
|
15
|
+
'E1M5' => 165, 'E1M6' => 180, 'E1M7' => 180, 'E1M8' => 30, 'E1M9' => 165
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
# Next map progression
|
|
19
|
+
NEXT_MAP = {
|
|
20
|
+
'E1M1' => 'E1M2', 'E1M2' => 'E1M3', 'E1M3' => 'E1M4', 'E1M4' => 'E1M5',
|
|
21
|
+
'E1M5' => 'E1M6', 'E1M6' => 'E1M7', 'E1M7' => 'E1M8', 'E1M8' => nil,
|
|
22
|
+
'E1M9' => 'E1M4'
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
# Counter animation speed (percentage points per tic)
|
|
26
|
+
COUNT_SPEED = 2
|
|
27
|
+
TICS_PER_COUNT = 1
|
|
28
|
+
|
|
29
|
+
attr_reader :finished, :next_map
|
|
30
|
+
|
|
31
|
+
def initialize(_wad, hud_graphics, stats)
|
|
32
|
+
@gfx = hud_graphics
|
|
33
|
+
@stats = stats # { map:, kills:, total_kills:, items:, total_items:, secrets:, total_secrets:, time_tics: }
|
|
34
|
+
@finished = false
|
|
35
|
+
@next_map = NEXT_MAP[stats[:map]]
|
|
36
|
+
@tic = 0
|
|
37
|
+
|
|
38
|
+
# Animated counters (count up from 0 to actual value)
|
|
39
|
+
@kill_count = 0
|
|
40
|
+
@item_count = 0
|
|
41
|
+
@secret_count = 0
|
|
42
|
+
@time_count = 0
|
|
43
|
+
@counting_done = false
|
|
44
|
+
|
|
45
|
+
# Target percentages
|
|
46
|
+
@kill_pct = @stats[:total_kills] > 0 ? (@stats[:kills] * 100 / @stats[:total_kills]) : 100
|
|
47
|
+
@item_pct = @stats[:total_items] > 0 ? (@stats[:items] * 100 / @stats[:total_items]) : 100
|
|
48
|
+
@secret_pct = @stats[:total_secrets] > 0 ? (@stats[:secrets] * 100 / @stats[:total_secrets]) : 100
|
|
49
|
+
@time_secs = @stats[:time_tics] / 35
|
|
50
|
+
|
|
51
|
+
@par_time = PAR_TIMES[stats[:map]] || 0
|
|
52
|
+
|
|
53
|
+
load_graphics
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def update
|
|
57
|
+
@tic += 1
|
|
58
|
+
return if @counting_done
|
|
59
|
+
|
|
60
|
+
# Animate counters
|
|
61
|
+
if @kill_count < @kill_pct
|
|
62
|
+
@kill_count = [@kill_count + COUNT_SPEED, @kill_pct].min
|
|
63
|
+
elsif @item_count < @item_pct
|
|
64
|
+
@item_count = [@item_count + COUNT_SPEED, @item_pct].min
|
|
65
|
+
elsif @secret_count < @secret_pct
|
|
66
|
+
@secret_count = [@secret_count + COUNT_SPEED, @secret_pct].min
|
|
67
|
+
elsif @time_count < @time_secs
|
|
68
|
+
@time_count = [@time_count + 3, @time_secs].min
|
|
69
|
+
else
|
|
70
|
+
@counting_done = true
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def render(framebuffer)
|
|
75
|
+
# Background
|
|
76
|
+
draw_background(framebuffer)
|
|
77
|
+
|
|
78
|
+
# "Finished" text + level name
|
|
79
|
+
draw_sprite(framebuffer, @wifinish, 64, 4) if @wifinish
|
|
80
|
+
level_idx = map_to_level_index(@stats[:map])
|
|
81
|
+
lv = @level_names[level_idx]
|
|
82
|
+
draw_sprite(framebuffer, lv, (320 - (lv&.width || 0)) / 2, 24) if lv
|
|
83
|
+
|
|
84
|
+
# Kill, Item, Secret percentages
|
|
85
|
+
y = 60
|
|
86
|
+
draw_sprite(framebuffer, @wiostk, 50, y) if @wiostk
|
|
87
|
+
draw_percent(framebuffer, 260, y, @kill_count)
|
|
88
|
+
|
|
89
|
+
y += 24
|
|
90
|
+
draw_sprite(framebuffer, @wiosti, 50, y) if @wiosti
|
|
91
|
+
draw_percent(framebuffer, 260, y, @item_count)
|
|
92
|
+
|
|
93
|
+
y += 24
|
|
94
|
+
draw_sprite(framebuffer, @wiosts, 50, y) if @wiosts
|
|
95
|
+
draw_percent(framebuffer, 260, y, @secret_count)
|
|
96
|
+
|
|
97
|
+
# Time
|
|
98
|
+
y += 30
|
|
99
|
+
draw_sprite(framebuffer, @witime, 16, y) if @witime
|
|
100
|
+
draw_time(framebuffer, 160, y, @time_count)
|
|
101
|
+
|
|
102
|
+
# Par time
|
|
103
|
+
draw_sprite(framebuffer, @wipar, 176, y) if @wipar
|
|
104
|
+
draw_time(framebuffer, 292, y, @par_time)
|
|
105
|
+
|
|
106
|
+
# "Entering" next level (after counting done)
|
|
107
|
+
if @counting_done && @next_map
|
|
108
|
+
y += 30
|
|
109
|
+
draw_sprite(framebuffer, @wienter, 64, y) if @wienter
|
|
110
|
+
next_idx = map_to_level_index(@next_map)
|
|
111
|
+
nlv = @level_names[next_idx]
|
|
112
|
+
draw_sprite(framebuffer, nlv, (320 - (nlv&.width || 0)) / 2, y + 18) if nlv
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# "Press any key" hint after counting
|
|
116
|
+
return unless @counting_done && (@tic / 17).even?
|
|
117
|
+
|
|
118
|
+
# Blink hint via skull
|
|
119
|
+
skull = @skulls[@tic / 8 % 2]
|
|
120
|
+
draw_sprite(framebuffer, skull, 144, 210) if skull
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def handle_key
|
|
124
|
+
if @counting_done
|
|
125
|
+
@finished = true
|
|
126
|
+
else
|
|
127
|
+
# Skip counting animation
|
|
128
|
+
@kill_count = @kill_pct
|
|
129
|
+
@item_count = @item_pct
|
|
130
|
+
@secret_count = @secret_pct
|
|
131
|
+
@time_count = @time_secs
|
|
132
|
+
@counting_done = true
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def map_to_level_index(map_name)
|
|
139
|
+
return 0 unless map_name
|
|
140
|
+
|
|
141
|
+
map_name[3].to_i - 1 # E1M1 -> 0, E1M2 -> 1, etc.
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def load_graphics
|
|
145
|
+
# Intermission number digits
|
|
146
|
+
@nums = (0..9).map { |n| load_patch("WINUM#{n}") }
|
|
147
|
+
@percent = load_patch('WIPCNT')
|
|
148
|
+
@colon = load_patch('WICOLON')
|
|
149
|
+
@minus = load_patch('WIMINUS')
|
|
150
|
+
|
|
151
|
+
# Labels
|
|
152
|
+
@wiostk = load_patch('WIOSTK') # "Kills"
|
|
153
|
+
@wiosti = load_patch('WIOSTI') # "Items"
|
|
154
|
+
@wiosts = load_patch('WIOSTS') # "Scrt" (Secrets)
|
|
155
|
+
@witime = load_patch('WITIME') # "Time"
|
|
156
|
+
@wipar = load_patch('WIPAR') # "Par"
|
|
157
|
+
@wifinish = load_patch('WIF') # "Finished"
|
|
158
|
+
@wienter = load_patch('WIENTER') # "Entering"
|
|
159
|
+
|
|
160
|
+
# Map background
|
|
161
|
+
@wimap = load_patch('WIMAP0')
|
|
162
|
+
|
|
163
|
+
# Level names (WILV00-WILV08)
|
|
164
|
+
@level_names = (0..8).map { |n| load_patch("WILV0#{n}") }
|
|
165
|
+
|
|
166
|
+
# Skull cursor
|
|
167
|
+
@skulls = [load_patch('M_SKULL1'), load_patch('M_SKULL2')]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def load_patch(name)
|
|
171
|
+
@gfx.load_graphic(name)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def draw_background(framebuffer)
|
|
175
|
+
return unless @wimap
|
|
176
|
+
|
|
177
|
+
draw_fullscreen(framebuffer, @wimap)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def draw_fullscreen(framebuffer, sprite)
|
|
181
|
+
return unless sprite
|
|
182
|
+
|
|
183
|
+
y_offset = [(Render::SCREEN_HEIGHT - sprite.height) / 2, 0].max
|
|
184
|
+
blit_sprite(framebuffer, sprite, 0, y_offset)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def draw_percent(framebuffer, right_x, y, value)
|
|
188
|
+
# Draw percent sign
|
|
189
|
+
draw_sprite(framebuffer, @percent, right_x, y) if @percent
|
|
190
|
+
|
|
191
|
+
# Draw number right-aligned before percent
|
|
192
|
+
draw_num_right(framebuffer, right_x - 2, y, value)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def draw_time(framebuffer, right_x, y, seconds)
|
|
196
|
+
mins = seconds / 60
|
|
197
|
+
secs = seconds % 60
|
|
198
|
+
|
|
199
|
+
# Draw seconds (2 digits, zero-padded)
|
|
200
|
+
draw_num_right(framebuffer, right_x, y, secs, pad: 2)
|
|
201
|
+
|
|
202
|
+
# Colon
|
|
203
|
+
colon_x = right_x - (num_width * 2) - 4
|
|
204
|
+
draw_sprite(framebuffer, @colon, colon_x, y) if @colon
|
|
205
|
+
|
|
206
|
+
# Minutes
|
|
207
|
+
draw_num_right(framebuffer, colon_x - 2, y, mins)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def num_width
|
|
211
|
+
@nums[0]&.width || 14
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def draw_num_right(framebuffer, right_x, y, value, pad: 0)
|
|
215
|
+
w = num_width
|
|
216
|
+
str = value.to_i.to_s
|
|
217
|
+
str = str.rjust(pad, '0') if pad > 0
|
|
218
|
+
x = right_x
|
|
219
|
+
str.reverse.each_char do |ch|
|
|
220
|
+
x -= w
|
|
221
|
+
digit = @nums[ch.to_i]
|
|
222
|
+
draw_sprite(framebuffer, digit, x, y) if digit
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def draw_sprite(framebuffer, sprite, x, y)
|
|
227
|
+
blit_sprite(framebuffer, sprite, x, y)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
@@ -7,7 +7,7 @@ module Doom
|
|
|
7
7
|
class ItemPickup
|
|
8
8
|
PLAYER_RADIUS = 16.0
|
|
9
9
|
THING_RADIUS = 20.0
|
|
10
|
-
PICKUP_DIST = PLAYER_RADIUS + THING_RADIUS
|
|
10
|
+
PICKUP_DIST = PLAYER_RADIUS + THING_RADIUS # 36 units (bounding box overlap)
|
|
11
11
|
|
|
12
12
|
# Item definitions: type => { category, value, ... }
|
|
13
13
|
ITEMS = {
|
|
@@ -26,9 +26,9 @@ module Doom
|
|
|
26
26
|
2049 => { cat: :ammo, ammo: :shells, amount: 20 }, # Box of shells
|
|
27
27
|
2010 => { cat: :ammo, ammo: :rockets, amount: 1 }, # Rocket
|
|
28
28
|
2046 => { cat: :ammo, ammo: :rockets, amount: 5 }, # Box of rockets
|
|
29
|
-
17
|
|
30
|
-
2047 => { cat: :ammo, ammo: :cells, amount: 100 },
|
|
31
|
-
8
|
|
29
|
+
17 => { cat: :ammo, ammo: :cells, amount: 20 }, # Cell charge
|
|
30
|
+
2047 => { cat: :ammo, ammo: :cells, amount: 100 }, # Cell pack
|
|
31
|
+
8 => { cat: :backpack }, # Backpack (doubles max ammo + some ammo)
|
|
32
32
|
|
|
33
33
|
# Health
|
|
34
34
|
2014 => { cat: :health, amount: 1, max: 200 }, # Health bonus (+1, up to 200)
|
|
@@ -37,132 +37,180 @@ module Doom
|
|
|
37
37
|
2013 => { cat: :health, amount: 100, max: 200 }, # Soul sphere
|
|
38
38
|
|
|
39
39
|
# Armor
|
|
40
|
-
2015 => { cat: :armor, amount: 1, max: 200 },
|
|
40
|
+
2015 => { cat: :armor, amount: 1, max: 200 }, # Armor bonus (+1, up to 200)
|
|
41
41
|
2018 => { cat: :armor, amount: 100, armor_type: 1 }, # Green armor (100%, absorbs 1/3)
|
|
42
42
|
2019 => { cat: :armor, amount: 200, armor_type: 2 }, # Blue armor (200%, absorbs 1/2)
|
|
43
43
|
|
|
44
44
|
# Keys
|
|
45
|
-
5
|
|
46
|
-
6
|
|
45
|
+
5 => { cat: :key, key: :blue_card },
|
|
46
|
+
6 => { cat: :key, key: :yellow_card },
|
|
47
47
|
13 => { cat: :key, key: :red_card },
|
|
48
48
|
40 => { cat: :key, key: :blue_skull },
|
|
49
49
|
39 => { cat: :key, key: :yellow_skull },
|
|
50
|
-
38 => { cat: :key, key: :red_skull }
|
|
50
|
+
38 => { cat: :key, key: :red_skull }
|
|
51
51
|
}.freeze
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
MESSAGETICS = 140 # 4 * TICRATE (4 seconds, matching Chocolate Doom)
|
|
54
|
+
FLASH_TICS = 8 # Yellow palette flash duration
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
attr_reader :picked_up, :pickup_message, :pickup_flash, :message_tics
|
|
57
|
+
attr_accessor :ammo_multiplier, :hidden_things
|
|
58
|
+
|
|
59
|
+
def initialize(map, hidden_things = {})
|
|
56
60
|
@map = map
|
|
57
|
-
@
|
|
58
|
-
@
|
|
61
|
+
@picked_up = {}
|
|
62
|
+
@hidden_things = hidden_things
|
|
63
|
+
@ammo_multiplier = 1
|
|
64
|
+
@pickup_message = nil
|
|
65
|
+
@pickup_flash = 0 # Yellow screen flash (short)
|
|
66
|
+
@message_tics = 0 # Message display timer (long)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Decay timers each tic
|
|
70
|
+
def update_flash
|
|
71
|
+
@pickup_flash -= 1 if @pickup_flash > 0
|
|
72
|
+
@message_tics -= 1 if @message_tics > 0
|
|
59
73
|
end
|
|
60
74
|
|
|
61
|
-
|
|
75
|
+
# Pick up for one player. `picked_up` stays global on purpose: in coop an
|
|
76
|
+
# item taken by anyone is gone for everyone, as in DOOM. Deathmatch
|
|
77
|
+
# respawning is a separate concern.
|
|
78
|
+
def update(player)
|
|
79
|
+
state = player.state
|
|
62
80
|
@map.things.each_with_index do |thing, idx|
|
|
81
|
+
next if @hidden_things[idx]
|
|
63
82
|
next if @picked_up[idx]
|
|
83
|
+
|
|
64
84
|
item = ITEMS[thing.type]
|
|
65
85
|
next unless item
|
|
66
86
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
dy = (player_y - thing.y).abs
|
|
87
|
+
dx = (player.x - thing.x).abs
|
|
88
|
+
dy = (player.y - thing.y).abs
|
|
70
89
|
next if dx >= PICKUP_DIST || dy >= PICKUP_DIST
|
|
71
90
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
91
|
+
next unless try_pickup(item, state)
|
|
92
|
+
|
|
93
|
+
@picked_up[idx] = true
|
|
94
|
+
@pickup_message = PICKUP_MESSAGES[thing.type]
|
|
95
|
+
@pickup_flash = FLASH_TICS
|
|
96
|
+
@message_tics = MESSAGETICS
|
|
75
97
|
end
|
|
76
98
|
end
|
|
77
99
|
|
|
100
|
+
PICKUP_MESSAGES = {
|
|
101
|
+
2001 => 'A SHOTGUN!', 2002 => 'A CHAINGUN!', 2003 => 'A ROCKET LAUNCHER!',
|
|
102
|
+
2004 => 'A PLASMA RIFLE!', 2005 => 'A CHAINSAW!', 2006 => 'A BFG9000!',
|
|
103
|
+
2007 => 'PICKED UP A CLIP.', 2048 => 'PICKED UP A BOX OF BULLETS.',
|
|
104
|
+
2008 => 'PICKED UP 4 SHOTGUN SHELLS.', 2049 => 'PICKED UP A BOX OF SHELLS.',
|
|
105
|
+
2010 => 'PICKED UP A ROCKET.', 2046 => 'PICKED UP A BOX OF ROCKETS.',
|
|
106
|
+
17 => 'PICKED UP AN ENERGY CELL.', 2047 => 'PICKED UP AN ENERGY CELL PACK.',
|
|
107
|
+
8 => 'PICKED UP A BACKPACK FULL OF AMMO!',
|
|
108
|
+
2011 => 'PICKED UP A STIMPACK.', 2012 => 'PICKED UP A MEDIKIT.',
|
|
109
|
+
2014 => 'PICKED UP A HEALTH BONUS.', 2015 => 'PICKED UP AN ARMOR BONUS.',
|
|
110
|
+
2018 => 'PICKED UP THE ARMOR.', 2019 => 'PICKED UP THE MEGAARMOR!',
|
|
111
|
+
2013 => 'SUPERCHARGE!',
|
|
112
|
+
5 => 'PICKED UP A BLUE KEYCARD.', 6 => 'PICKED UP A YELLOW KEYCARD.',
|
|
113
|
+
13 => 'PICKED UP A RED KEYCARD.', 40 => 'PICKED UP A BLUE SKULL KEY.',
|
|
114
|
+
39 => 'PICKED UP A YELLOW SKULL KEY.', 38 => 'PICKED UP A RED SKULL KEY.'
|
|
115
|
+
}.freeze
|
|
116
|
+
|
|
78
117
|
private
|
|
79
118
|
|
|
80
|
-
def try_pickup(item)
|
|
119
|
+
def try_pickup(item, state)
|
|
81
120
|
case item[:cat]
|
|
82
121
|
when :weapon
|
|
83
|
-
give_weapon(item)
|
|
122
|
+
give_weapon(item, state)
|
|
84
123
|
when :ammo
|
|
85
|
-
give_ammo(item[:ammo], item[:amount])
|
|
124
|
+
give_ammo(item[:ammo], item[:amount], state)
|
|
86
125
|
when :backpack
|
|
87
|
-
give_backpack
|
|
126
|
+
give_backpack(state)
|
|
88
127
|
when :health
|
|
89
|
-
give_health(item[:amount], item[:max])
|
|
128
|
+
give_health(item[:amount], item[:max], state)
|
|
90
129
|
when :armor
|
|
91
|
-
give_armor(item)
|
|
130
|
+
give_armor(item, state)
|
|
92
131
|
when :key
|
|
93
|
-
give_key(item[:key])
|
|
132
|
+
give_key(item[:key], state)
|
|
94
133
|
else
|
|
95
134
|
false
|
|
96
135
|
end
|
|
97
136
|
end
|
|
98
137
|
|
|
99
|
-
def give_weapon(item)
|
|
138
|
+
def give_weapon(item, state)
|
|
100
139
|
weapon_idx = item[:weapon]
|
|
101
|
-
had_weapon =
|
|
140
|
+
had_weapon = state.has_weapons[weapon_idx]
|
|
102
141
|
|
|
103
|
-
|
|
104
|
-
ammo_given = item[:ammo] ? give_ammo(item[:ammo], item[:amount]) : false
|
|
142
|
+
state.has_weapons[weapon_idx] = true
|
|
143
|
+
ammo_given = item[:ammo] ? give_ammo(item[:ammo], item[:amount], state) : false
|
|
105
144
|
|
|
106
145
|
unless had_weapon
|
|
107
|
-
|
|
146
|
+
state.switch_weapon(weapon_idx) unless state.attacking
|
|
108
147
|
return true
|
|
109
148
|
end
|
|
110
149
|
|
|
111
150
|
ammo_given
|
|
112
151
|
end
|
|
113
152
|
|
|
114
|
-
def give_ammo(type, amount)
|
|
153
|
+
def give_ammo(type, amount, state)
|
|
154
|
+
amount *= @ammo_multiplier
|
|
115
155
|
case type
|
|
116
156
|
when :bullets
|
|
117
|
-
return false if
|
|
118
|
-
|
|
157
|
+
return false if state.ammo_bullets >= state.max_bullets
|
|
158
|
+
|
|
159
|
+
state.ammo_bullets = [state.ammo_bullets + amount, state.max_bullets].min
|
|
119
160
|
when :shells
|
|
120
|
-
return false if
|
|
121
|
-
|
|
161
|
+
return false if state.ammo_shells >= state.max_shells
|
|
162
|
+
|
|
163
|
+
state.ammo_shells = [state.ammo_shells + amount, state.max_shells].min
|
|
122
164
|
when :rockets
|
|
123
|
-
return false if
|
|
124
|
-
|
|
165
|
+
return false if state.ammo_rockets >= state.max_rockets
|
|
166
|
+
|
|
167
|
+
state.ammo_rockets = [state.ammo_rockets + amount, state.max_rockets].min
|
|
125
168
|
when :cells
|
|
126
|
-
return false if
|
|
127
|
-
|
|
169
|
+
return false if state.ammo_cells >= state.max_cells
|
|
170
|
+
|
|
171
|
+
state.ammo_cells = [state.ammo_cells + amount, state.max_cells].min
|
|
128
172
|
end
|
|
129
173
|
true
|
|
130
174
|
end
|
|
131
175
|
|
|
132
|
-
def give_backpack
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
give_ammo(:bullets, 10)
|
|
138
|
-
give_ammo(:shells, 4)
|
|
139
|
-
give_ammo(:rockets, 1)
|
|
140
|
-
give_ammo(:cells, 20)
|
|
176
|
+
def give_backpack(state)
|
|
177
|
+
state.max_bullets = 400
|
|
178
|
+
state.max_shells = 100
|
|
179
|
+
state.max_rockets = 100
|
|
180
|
+
state.max_cells = 600
|
|
181
|
+
give_ammo(:bullets, 10, state)
|
|
182
|
+
give_ammo(:shells, 4, state)
|
|
183
|
+
give_ammo(:rockets, 1, state)
|
|
184
|
+
give_ammo(:cells, 20, state)
|
|
141
185
|
true
|
|
142
186
|
end
|
|
143
187
|
|
|
144
|
-
def give_health(amount, max)
|
|
145
|
-
return false if
|
|
146
|
-
|
|
188
|
+
def give_health(amount, max, state)
|
|
189
|
+
return false if state.health >= max
|
|
190
|
+
|
|
191
|
+
state.health = [state.health + amount, max].min
|
|
147
192
|
true
|
|
148
193
|
end
|
|
149
194
|
|
|
150
|
-
def give_armor(item)
|
|
195
|
+
def give_armor(item, state)
|
|
151
196
|
if item[:armor_type]
|
|
152
197
|
# Green/blue armor: only pick up if better than current
|
|
153
|
-
return false if
|
|
154
|
-
|
|
198
|
+
return false if state.armor >= item[:amount]
|
|
199
|
+
|
|
200
|
+
state.armor = item[:amount]
|
|
155
201
|
else
|
|
156
202
|
# Armor bonus: +1, up to max
|
|
157
|
-
return false if
|
|
158
|
-
|
|
203
|
+
return false if state.armor >= item[:max]
|
|
204
|
+
|
|
205
|
+
state.armor = [state.armor + item[:amount], item[:max]].min
|
|
159
206
|
end
|
|
160
207
|
true
|
|
161
208
|
end
|
|
162
209
|
|
|
163
|
-
def give_key(key)
|
|
164
|
-
return false if
|
|
165
|
-
|
|
210
|
+
def give_key(key, state)
|
|
211
|
+
return false if state.keys[key]
|
|
212
|
+
|
|
213
|
+
state.keys[key] = true
|
|
166
214
|
true
|
|
167
215
|
end
|
|
168
216
|
end
|