doom 0.6.0 → 0.8.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/bin/doom +5 -2
- data/lib/doom/game/combat.rb +226 -40
- data/lib/doom/game/intermission.rb +248 -0
- data/lib/doom/game/item_pickup.rb +40 -4
- data/lib/doom/game/menu.rb +342 -0
- data/lib/doom/game/monster_ai.rb +210 -11
- data/lib/doom/game/player_state.rb +10 -1
- data/lib/doom/game/sector_actions.rb +376 -15
- data/lib/doom/game/sound_engine.rb +201 -0
- data/lib/doom/platform/gosu_window.rb +460 -52
- data/lib/doom/render/font.rb +78 -0
- data/lib/doom/render/renderer.rb +361 -33
- data/lib/doom/render/screen_melt.rb +71 -0
- data/lib/doom/render/weapon_renderer.rb +11 -12
- data/lib/doom/version.rb +1 -1
- data/lib/doom/wad/reader.rb +52 -0
- data/lib/doom/wad/sound.rb +85 -0
- data/lib/doom/wad/sprite.rb +29 -4
- data/lib/doom/wad/texture.rb +1 -1
- data/lib/doom.rb +47 -5
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e59cb32b1487d61b1c310ee5eae7ec30a702c64129c7fe5e18e1c06204d08208
|
|
4
|
+
data.tar.gz: f8f5450c3c37d661618234cf0abe80a5f0cd796aaa13694ee35d786ef25c5728
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a65c31832aff4838f924eb1263bda1f16ef6e820da9d776fa759656fdb9a58d83b2146ea30d5e0c236d1fc9e39206ad14312191f3bf5db70597cec815aef113
|
|
7
|
+
data.tar.gz: f6215f28a05e9924b9864f2f51d36cd694c4abfe7c0ad1618116486f62c72d26b0d7f609e945725b2218ee4b3e77e13eb3316153396977bf773673185d93ba40
|
data/bin/doom
CHANGED
|
@@ -8,8 +8,11 @@ unless ARGV.delete('--no-yjit')
|
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
# Parse flags
|
|
12
|
+
rubykaigi_mode = ARGV.delete('--rubykaigi') || ARGV.delete('--kaigi')
|
|
13
|
+
|
|
11
14
|
# Parse arguments before loading heavy dependencies
|
|
12
|
-
wad_path = ARGV
|
|
15
|
+
wad_path = ARGV.find { |a| !a.start_with?('-') }
|
|
13
16
|
|
|
14
17
|
# Show help (before loading anything)
|
|
15
18
|
if wad_path == '-h' || wad_path == '--help'
|
|
@@ -51,7 +54,7 @@ require_relative '../lib/doom'
|
|
|
51
54
|
begin
|
|
52
55
|
# Find or download WAD
|
|
53
56
|
wad_path = Doom::WadDownloader.ensure_wad_available(wad_path)
|
|
54
|
-
Doom.run(wad_path)
|
|
57
|
+
Doom.run(wad_path, rubykaigi: rubykaigi_mode)
|
|
55
58
|
rescue Doom::WadDownloader::DownloadError => e
|
|
56
59
|
puts "Error: #{e.message}"
|
|
57
60
|
exit 1
|
data/lib/doom/game/combat.rb
CHANGED
|
@@ -22,14 +22,22 @@ module Doom
|
|
|
22
22
|
64 => 700, # Archvile
|
|
23
23
|
71 => 400, # Pain Elemental
|
|
24
24
|
84 => 20, # Wolfenstein SS
|
|
25
|
+
2035 => 20, # Explosive barrel
|
|
25
26
|
}.freeze
|
|
26
27
|
|
|
27
28
|
MONSTER_RADIUS = {
|
|
28
29
|
3004 => 20, 9 => 20, 3001 => 20, 3002 => 30, 58 => 30,
|
|
29
30
|
3003 => 24, 69 => 24, 3005 => 31, 3006 => 16, 16 => 40,
|
|
30
31
|
7 => 128, 65 => 20, 64 => 20, 71 => 31, 84 => 20,
|
|
32
|
+
2035 => 10, # Barrel
|
|
31
33
|
}.freeze
|
|
32
34
|
|
|
35
|
+
# Barrel (explosive, not a monster but damageable)
|
|
36
|
+
BARREL_TYPE = 2035
|
|
37
|
+
BARREL_HP = 20
|
|
38
|
+
BARREL_SPLASH_RADIUS = 128.0
|
|
39
|
+
BARREL_SPLASH_DAMAGE = 128
|
|
40
|
+
|
|
33
41
|
# Normal death frame sequences per sprite prefix (rotation 0 only)
|
|
34
42
|
# Identified by sprite heights: frames go from standing height to flat on ground
|
|
35
43
|
DEATH_FRAMES = {
|
|
@@ -46,10 +54,19 @@ module Doom
|
|
|
46
54
|
'CPOS' => %w[H I J K L M N], # Heavy Weapon Dude
|
|
47
55
|
'PAIN' => %w[H I J K L M], # Pain Elemental
|
|
48
56
|
'SSWV' => %w[I J K L M], # Wolfenstein SS
|
|
57
|
+
'BEXP' => %w[A B C D E], # Barrel explosion
|
|
49
58
|
}.freeze
|
|
50
59
|
|
|
51
60
|
DEATH_ANIM_TICS = 6 # Tics per death frame
|
|
52
61
|
|
|
62
|
+
# Pain chance per monster (out of 256, from mobjinfo)
|
|
63
|
+
PAIN_CHANCE = {
|
|
64
|
+
3004 => 200, 9 => 170, 3001 => 200, 3002 => 180, 58 => 180,
|
|
65
|
+
3003 => 50, 69 => 50, 3005 => 128, 3006 => 256, 16 => 40,
|
|
66
|
+
7 => 40, 65 => 170, 64 => 10, 71 => 128, 84 => 170,
|
|
67
|
+
}.freeze
|
|
68
|
+
PAIN_DURATION = 6 # Tics monster is stunned when in pain
|
|
69
|
+
|
|
53
70
|
# Projectile constants
|
|
54
71
|
ROCKET_SPEED = 20.0 # Map units per tic (matches DOOM's mobjinfo MISSILESPEED)
|
|
55
72
|
ROCKET_DAMAGE = 20 # Direct hit base (DOOM: 1d8 * 20)
|
|
@@ -57,43 +74,121 @@ module Doom
|
|
|
57
74
|
SPLASH_RADIUS = 128.0 # Splash damage radius
|
|
58
75
|
SPLASH_DAMAGE = 128 # Max splash damage at center
|
|
59
76
|
|
|
60
|
-
|
|
77
|
+
# Monster projectile definitions
|
|
78
|
+
MONSTER_PROJECTILES = {
|
|
79
|
+
imp: { sprite: 'BAL1', speed: 10.0, damage: [3, 24], radius: 6, splash: false },
|
|
80
|
+
baron: { sprite: 'BAL7', speed: 15.0, damage: [8, 64], radius: 6, splash: false },
|
|
81
|
+
caco: { sprite: 'BAL2', speed: 10.0, damage: [5, 40], radius: 6, splash: false },
|
|
82
|
+
}.freeze
|
|
83
|
+
|
|
84
|
+
# Map monster type to projectile type
|
|
85
|
+
MONSTER_PROJECTILE_TYPE = {
|
|
86
|
+
3001 => :imp, # Imp
|
|
87
|
+
3003 => :baron, # Baron
|
|
88
|
+
69 => :baron, # Hell Knight
|
|
89
|
+
3005 => :caco, # Cacodemon
|
|
90
|
+
}.freeze
|
|
91
|
+
|
|
92
|
+
Projectile = Struct.new(:x, :y, :z, :dx, :dy, :dz, :type, :spawn_tic, :sprite_prefix, :target)
|
|
61
93
|
|
|
62
94
|
# Weapon damage: DOOM does (P_Random()%3 + 1) * multiplier
|
|
63
95
|
# Pistol/chaingun: 1*5..3*5 = 5-15 per bullet
|
|
64
96
|
# Shotgun: 7 pellets, each 1*5..3*5 = 5-15
|
|
65
97
|
# Fist/chainsaw: 1*2..3*2 = 2-10
|
|
66
98
|
|
|
67
|
-
def initialize(map, player_state, sprites)
|
|
99
|
+
def initialize(map, player_state, sprites, hidden_things = {}, sound_engine = nil)
|
|
68
100
|
@map = map
|
|
69
101
|
@player = player_state
|
|
70
102
|
@sprites = sprites
|
|
103
|
+
@hidden_things = hidden_things
|
|
104
|
+
@sound = sound_engine
|
|
71
105
|
@monster_hp = {} # thing_idx => current HP
|
|
72
106
|
@dead_things = {} # thing_idx => { tic: death_start_tic, prefix: sprite_prefix }
|
|
107
|
+
@pain_until = {} # thing_idx => tic when pain ends
|
|
73
108
|
@projectiles = [] # Active projectiles in flight
|
|
74
109
|
@explosions = [] # Active explosions (for rendering)
|
|
110
|
+
@puffs = [] # Bullet puff effects
|
|
111
|
+
@player_x = 0.0
|
|
112
|
+
@player_y = 0.0
|
|
113
|
+
@player_z = 0.0
|
|
75
114
|
@tic = 0
|
|
76
115
|
end
|
|
77
116
|
|
|
78
|
-
|
|
117
|
+
def update_player_pos(x, y, z = nil)
|
|
118
|
+
@player_x = x
|
|
119
|
+
@player_y = y
|
|
120
|
+
@player_z = z if z
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Spawn a monster projectile (fireball, etc.)
|
|
124
|
+
# Matches Chocolate Doom's P_SpawnMissile: calculates momz for vertical aim
|
|
125
|
+
def spawn_monster_projectile(monster_x, monster_y, monster_z, monster_type, damage_multiplier)
|
|
126
|
+
proj_type = MONSTER_PROJECTILE_TYPE[monster_type]
|
|
127
|
+
return unless proj_type
|
|
128
|
+
|
|
129
|
+
info = MONSTER_PROJECTILES[proj_type]
|
|
130
|
+
return unless info
|
|
131
|
+
|
|
132
|
+
dx = @player_x - monster_x
|
|
133
|
+
dy = @player_y - monster_y
|
|
134
|
+
dist = Math.sqrt(dx * dx + dy * dy)
|
|
135
|
+
return if dist < 1
|
|
136
|
+
|
|
137
|
+
# Normalize direction and apply speed
|
|
138
|
+
speed = info[:speed]
|
|
139
|
+
ndx = dx / dist * speed
|
|
140
|
+
ndy = dy / dist * speed
|
|
141
|
+
|
|
142
|
+
# P_SpawnMissile: momz = (target.z - source.z) / (dist / speed)
|
|
143
|
+
# This makes the projectile arc toward the target's height
|
|
144
|
+
target_z = @player_z - 16 # Aim at player center (z + height/2, roughly)
|
|
145
|
+
travel_tics = dist / speed
|
|
146
|
+
travel_tics = 1.0 if travel_tics < 1.0
|
|
147
|
+
ndz = (target_z - monster_z) / travel_tics
|
|
148
|
+
|
|
149
|
+
@projectiles << Projectile.new(
|
|
150
|
+
monster_x + ndx * 2, monster_y + ndy * 2, monster_z,
|
|
151
|
+
ndx, ndy, ndz, proj_type, @tic, info[:sprite], :player
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
attr_reader :dead_things, :projectiles, :explosions, :puffs
|
|
156
|
+
|
|
157
|
+
def in_pain?(thing_idx)
|
|
158
|
+
@pain_until[thing_idx] && @tic < @pain_until[thing_idx]
|
|
159
|
+
end
|
|
79
160
|
|
|
80
161
|
def dead?(thing_idx)
|
|
81
162
|
@dead_things.key?(thing_idx)
|
|
82
163
|
end
|
|
83
164
|
|
|
84
|
-
# Get the current death frame sprite for a dead monster
|
|
165
|
+
# Get the current death frame sprite for a dead monster/barrel
|
|
85
166
|
def death_sprite(thing_idx, thing_type, viewer_angle, thing_angle)
|
|
86
167
|
info = @dead_things[thing_idx]
|
|
87
168
|
return nil unless info
|
|
88
169
|
|
|
89
|
-
|
|
170
|
+
prefix = info[:prefix]
|
|
171
|
+
frames = DEATH_FRAMES[prefix]
|
|
90
172
|
return nil unless frames
|
|
91
173
|
|
|
92
174
|
elapsed = @tic - info[:tic]
|
|
93
|
-
frame_idx =
|
|
175
|
+
frame_idx = elapsed / DEATH_ANIM_TICS
|
|
176
|
+
|
|
177
|
+
# Barrels disappear after explosion animation (S_NULL in Chocolate Doom)
|
|
178
|
+
if thing_type == BARREL_TYPE && frame_idx >= frames.size
|
|
179
|
+
return nil
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
frame_idx = frame_idx.clamp(0, frames.size - 1)
|
|
94
183
|
frame_letter = frames[frame_idx]
|
|
95
184
|
|
|
96
|
-
|
|
185
|
+
# Use prefix directly if it differs from the thing's sprite (e.g. BEXP for barrels)
|
|
186
|
+
thing_prefix = @sprites.prefix_for(thing_type)
|
|
187
|
+
if prefix != thing_prefix
|
|
188
|
+
@sprites.get_frame_by_prefix(prefix, frame_letter)
|
|
189
|
+
else
|
|
190
|
+
@sprites.get_frame(thing_type, frame_letter, viewer_angle, thing_angle)
|
|
191
|
+
end
|
|
97
192
|
end
|
|
98
193
|
|
|
99
194
|
# Called each game tic
|
|
@@ -101,6 +196,7 @@ module Doom
|
|
|
101
196
|
@tic += 1
|
|
102
197
|
update_projectiles
|
|
103
198
|
update_explosions
|
|
199
|
+
@puffs.reject! { |p| @tic - p[:tic] > 12 }
|
|
104
200
|
end
|
|
105
201
|
|
|
106
202
|
# Fire the current weapon
|
|
@@ -122,44 +218,74 @@ module Doom
|
|
|
122
218
|
private
|
|
123
219
|
|
|
124
220
|
def spawn_rocket(px, py, pz, cos_a, sin_a)
|
|
125
|
-
# Spawn slightly ahead of the player
|
|
126
221
|
@projectiles << Projectile.new(
|
|
127
222
|
px + cos_a * 20, py + sin_a * 20, pz,
|
|
128
|
-
cos_a * ROCKET_SPEED, sin_a * ROCKET_SPEED,
|
|
129
|
-
:rocket, @tic
|
|
223
|
+
cos_a * ROCKET_SPEED, sin_a * ROCKET_SPEED, 0.0,
|
|
224
|
+
:rocket, @tic, 'MISL', :monsters
|
|
130
225
|
)
|
|
131
226
|
end
|
|
132
227
|
|
|
133
228
|
def update_projectiles
|
|
134
229
|
@projectiles.reject! do |proj|
|
|
135
|
-
# Move projectile
|
|
136
230
|
new_x = proj.x + proj.dx
|
|
137
231
|
new_y = proj.y + proj.dy
|
|
232
|
+
new_z = proj.z + (proj.dz || 0)
|
|
138
233
|
|
|
139
|
-
# Check wall collision
|
|
140
234
|
hit_wall = hits_wall?(proj.x, proj.y, new_x, new_y)
|
|
141
235
|
|
|
142
|
-
# Check
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
236
|
+
# Check if projectile hit the floor or ceiling
|
|
237
|
+
sector = @map.sector_at(new_x, new_y)
|
|
238
|
+
if sector
|
|
239
|
+
hit_wall = true if new_z <= sector.floor_height || new_z >= sector.ceiling_height
|
|
240
|
+
end
|
|
241
|
+
hit = false
|
|
242
|
+
|
|
243
|
+
if proj.target == :monsters
|
|
244
|
+
# Player projectile: check monster collision
|
|
245
|
+
hit_monster = nil
|
|
246
|
+
@map.things.each_with_index do |thing, idx|
|
|
247
|
+
next unless MONSTER_HP[thing.type]
|
|
248
|
+
next if @dead_things[idx]
|
|
249
|
+
radius = (MONSTER_RADIUS[thing.type] || 20) + ROCKET_RADIUS
|
|
250
|
+
dx = new_x - thing.x
|
|
251
|
+
dy = new_y - thing.y
|
|
252
|
+
if dx * dx + dy * dy < radius * radius
|
|
253
|
+
hit_monster = idx
|
|
254
|
+
break
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
if hit_wall || hit_monster
|
|
259
|
+
explode(new_x, new_y, hit_monster) if proj.type == :rocket
|
|
260
|
+
hit_monster ? apply_damage(hit_monster, (rand(8) + 1) * 5) : nil unless proj.type == :rocket
|
|
261
|
+
hit = true
|
|
262
|
+
end
|
|
263
|
+
elsif proj.target == :player
|
|
264
|
+
# Monster projectile: check player collision
|
|
265
|
+
player_radius = 16
|
|
266
|
+
dx = new_x - @player_x
|
|
267
|
+
dy = new_y - @player_y
|
|
268
|
+
if hit_wall || (dx * dx + dy * dy < (player_radius + 6) ** 2)
|
|
269
|
+
unless hit_wall
|
|
270
|
+
info = MONSTER_PROJECTILES[proj.type]
|
|
271
|
+
if info
|
|
272
|
+
min_d, max_d = info[:damage]
|
|
273
|
+
@player.take_damage(rand(min_d..max_d))
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
# Spawn fireball explosion
|
|
277
|
+
@explosions << { x: new_x, y: new_y, z: proj.z, tic: @tic, sprite: proj.sprite_prefix }
|
|
278
|
+
hit = true
|
|
153
279
|
end
|
|
154
280
|
end
|
|
155
281
|
|
|
156
|
-
if
|
|
157
|
-
|
|
158
|
-
true # Remove projectile
|
|
282
|
+
if hit
|
|
283
|
+
true
|
|
159
284
|
else
|
|
160
285
|
proj.x = new_x
|
|
161
286
|
proj.y = new_y
|
|
162
|
-
|
|
287
|
+
proj.z = new_z
|
|
288
|
+
false
|
|
163
289
|
end
|
|
164
290
|
end
|
|
165
291
|
end
|
|
@@ -188,7 +314,7 @@ module Doom
|
|
|
188
314
|
end
|
|
189
315
|
|
|
190
316
|
# Spawn explosion visual
|
|
191
|
-
@explosions << { x: x, y: y, tic: @tic }
|
|
317
|
+
@explosions << { x: x, y: y, tic: @tic, sprite: 'MISL' }
|
|
192
318
|
end
|
|
193
319
|
|
|
194
320
|
def update_explosions
|
|
@@ -198,17 +324,21 @@ module Doom
|
|
|
198
324
|
|
|
199
325
|
def hits_wall?(x1, y1, x2, y2)
|
|
200
326
|
@map.linedefs.each do |ld|
|
|
201
|
-
# One-sided walls always block
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
327
|
+
# One-sided walls always block projectiles
|
|
328
|
+
if ld.sidedef_left == 0xFFFF
|
|
329
|
+
blocks = true
|
|
330
|
+
elsif ld.sidedef_left < 0xFFFF
|
|
331
|
+
# Two-sided: only block if opening is too small for a projectile
|
|
332
|
+
# BLOCKING flag (0x0001) stops players/monsters but NOT projectiles
|
|
205
333
|
front = @map.sidedefs[ld.sidedef_right]
|
|
206
334
|
back = @map.sidedefs[ld.sidedef_left]
|
|
207
335
|
fs = @map.sectors[front.sector]
|
|
208
336
|
bs = @map.sectors[back.sector]
|
|
209
337
|
max_floor = [fs.floor_height, bs.floor_height].max
|
|
210
338
|
min_ceil = [fs.ceiling_height, bs.ceiling_height].min
|
|
211
|
-
blocks = (min_ceil - max_floor) <
|
|
339
|
+
blocks = (min_ceil - max_floor) < 1 # Closed door/wall
|
|
340
|
+
else
|
|
341
|
+
next
|
|
212
342
|
end
|
|
213
343
|
next unless blocks
|
|
214
344
|
|
|
@@ -252,6 +382,7 @@ module Doom
|
|
|
252
382
|
best_dist = wall_dist
|
|
253
383
|
|
|
254
384
|
@map.things.each_with_index do |thing, idx|
|
|
385
|
+
next if @hidden_things[idx]
|
|
255
386
|
next unless MONSTER_HP[thing.type]
|
|
256
387
|
next if @dead_things[idx]
|
|
257
388
|
|
|
@@ -263,6 +394,12 @@ module Doom
|
|
|
263
394
|
end
|
|
264
395
|
end
|
|
265
396
|
|
|
397
|
+
# Spawn bullet puff at hit location
|
|
398
|
+
puff_x = px + ca * best_dist
|
|
399
|
+
puff_y = py + sa * best_dist
|
|
400
|
+
puff_z = @player_z
|
|
401
|
+
@puffs << { x: puff_x, y: puff_y, z: puff_z, tic: @tic }
|
|
402
|
+
|
|
266
403
|
if best_idx
|
|
267
404
|
damage = (rand(3) + 1) * multiplier
|
|
268
405
|
apply_damage(best_idx, damage)
|
|
@@ -306,8 +443,54 @@ module Doom
|
|
|
306
443
|
@monster_hp[thing_idx] -= damage
|
|
307
444
|
|
|
308
445
|
if @monster_hp[thing_idx] <= 0
|
|
309
|
-
|
|
310
|
-
|
|
446
|
+
return if @dead_things[thing_idx] # Already dead
|
|
447
|
+
|
|
448
|
+
if thing.type == BARREL_TYPE
|
|
449
|
+
@dead_things[thing_idx] = { tic: @tic, prefix: 'BEXP' }
|
|
450
|
+
@sound&.explosion
|
|
451
|
+
barrel_explode(thing.x, thing.y, thing_idx)
|
|
452
|
+
else
|
|
453
|
+
prefix = @sprites.prefix_for(thing.type)
|
|
454
|
+
@dead_things[thing_idx] = { tic: @tic, prefix: prefix } if prefix
|
|
455
|
+
@sound&.monster_death(thing.type)
|
|
456
|
+
end
|
|
457
|
+
else
|
|
458
|
+
# Pain state: monster flinches (not barrels)
|
|
459
|
+
if thing.type != BARREL_TYPE
|
|
460
|
+
pain_chance = PAIN_CHANCE[thing.type] || 128
|
|
461
|
+
if rand(256) < pain_chance
|
|
462
|
+
@pain_until[thing_idx] = @tic + PAIN_DURATION
|
|
463
|
+
@sound&.monster_pain(thing.type)
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def barrel_explode(x, y, barrel_idx)
|
|
470
|
+
@explosions << { x: x, y: y, tic: @tic, sprite: 'MISL' }
|
|
471
|
+
|
|
472
|
+
# Splash damage to monsters and other barrels (chain reactions!)
|
|
473
|
+
@map.things.each_with_index do |thing, idx|
|
|
474
|
+
next unless MONSTER_HP[thing.type]
|
|
475
|
+
next if @dead_things[idx]
|
|
476
|
+
next if idx == barrel_idx
|
|
477
|
+
|
|
478
|
+
dx = x - thing.x
|
|
479
|
+
dy = y - thing.y
|
|
480
|
+
dist = Math.sqrt(dx * dx + dy * dy)
|
|
481
|
+
next if dist >= BARREL_SPLASH_RADIUS
|
|
482
|
+
|
|
483
|
+
damage = ((BARREL_SPLASH_DAMAGE * (1.0 - dist / BARREL_SPLASH_RADIUS))).to_i
|
|
484
|
+
apply_damage(idx, damage) if damage > 0
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Splash damage to player
|
|
488
|
+
dx = x - @player_x
|
|
489
|
+
dy = y - @player_y
|
|
490
|
+
dist = Math.sqrt(dx * dx + dy * dy)
|
|
491
|
+
if dist < BARREL_SPLASH_RADIUS
|
|
492
|
+
damage = ((BARREL_SPLASH_DAMAGE * (1.0 - dist / BARREL_SPLASH_RADIUS))).to_i
|
|
493
|
+
@player.take_damage(damage) if damage > 0
|
|
311
494
|
end
|
|
312
495
|
end
|
|
313
496
|
|
|
@@ -318,18 +501,21 @@ module Doom
|
|
|
318
501
|
v1 = @map.vertices[ld.v1]
|
|
319
502
|
v2 = @map.vertices[ld.v2]
|
|
320
503
|
|
|
321
|
-
# One-sided always blocks
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
504
|
+
# One-sided always blocks hitscan
|
|
505
|
+
if ld.sidedef_left == 0xFFFF
|
|
506
|
+
blocks = true
|
|
507
|
+
elsif ld.sidedef_left < 0xFFFF
|
|
508
|
+
# Two-sided: only blocks if opening is too small
|
|
509
|
+
# BLOCKING flag does NOT stop hitscan (only affects movement)
|
|
325
510
|
front = @map.sidedefs[ld.sidedef_right]
|
|
326
511
|
back = @map.sidedefs[ld.sidedef_left]
|
|
327
512
|
fs = @map.sectors[front.sector]
|
|
328
513
|
bs = @map.sectors[back.sector]
|
|
329
|
-
# Blocks if opening is too small (step or low ceiling)
|
|
330
514
|
max_floor = [fs.floor_height, bs.floor_height].max
|
|
331
515
|
min_ceil = [fs.ceiling_height, bs.ceiling_height].min
|
|
332
516
|
blocks = (min_ceil - max_floor) < 56
|
|
517
|
+
else
|
|
518
|
+
next
|
|
333
519
|
end
|
|
334
520
|
next unless blocks
|
|
335
521
|
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Game
|
|
5
|
+
# Intermission screen shown between levels.
|
|
6
|
+
# Displays kill%, item%, secret%, time, and par time.
|
|
7
|
+
class Intermission
|
|
8
|
+
# Episode 1 par times in seconds (from Chocolate Doom)
|
|
9
|
+
PAR_TIMES = {
|
|
10
|
+
'E1M1' => 30, 'E1M2' => 75, 'E1M3' => 120, 'E1M4' => 90,
|
|
11
|
+
'E1M5' => 165, 'E1M6' => 180, 'E1M7' => 180, 'E1M8' => 30, 'E1M9' => 165,
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
# Next map progression
|
|
15
|
+
NEXT_MAP = {
|
|
16
|
+
'E1M1' => 'E1M2', 'E1M2' => 'E1M3', 'E1M3' => 'E1M4', 'E1M4' => 'E1M5',
|
|
17
|
+
'E1M5' => 'E1M6', 'E1M6' => 'E1M7', 'E1M7' => 'E1M8', 'E1M8' => nil,
|
|
18
|
+
'E1M9' => 'E1M4',
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# Counter animation speed (percentage points per tic)
|
|
22
|
+
COUNT_SPEED = 2
|
|
23
|
+
TICS_PER_COUNT = 1
|
|
24
|
+
|
|
25
|
+
attr_reader :finished, :next_map
|
|
26
|
+
|
|
27
|
+
def initialize(wad, hud_graphics, stats)
|
|
28
|
+
@wad = wad
|
|
29
|
+
@gfx = hud_graphics
|
|
30
|
+
@stats = stats # { map:, kills:, total_kills:, items:, total_items:, secrets:, total_secrets:, time_tics: }
|
|
31
|
+
@finished = false
|
|
32
|
+
@next_map = NEXT_MAP[stats[:map]]
|
|
33
|
+
@tic = 0
|
|
34
|
+
|
|
35
|
+
# Animated counters (count up from 0 to actual value)
|
|
36
|
+
@kill_count = 0
|
|
37
|
+
@item_count = 0
|
|
38
|
+
@secret_count = 0
|
|
39
|
+
@time_count = 0
|
|
40
|
+
@counting_done = false
|
|
41
|
+
|
|
42
|
+
# Target percentages
|
|
43
|
+
@kill_pct = @stats[:total_kills] > 0 ? (@stats[:kills] * 100 / @stats[:total_kills]) : 100
|
|
44
|
+
@item_pct = @stats[:total_items] > 0 ? (@stats[:items] * 100 / @stats[:total_items]) : 100
|
|
45
|
+
@secret_pct = @stats[:total_secrets] > 0 ? (@stats[:secrets] * 100 / @stats[:total_secrets]) : 100
|
|
46
|
+
@time_secs = @stats[:time_tics] / 35
|
|
47
|
+
|
|
48
|
+
@par_time = PAR_TIMES[stats[:map]] || 0
|
|
49
|
+
|
|
50
|
+
load_graphics
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def update
|
|
54
|
+
@tic += 1
|
|
55
|
+
return if @counting_done
|
|
56
|
+
|
|
57
|
+
# Animate counters
|
|
58
|
+
if @kill_count < @kill_pct
|
|
59
|
+
@kill_count = [@kill_count + COUNT_SPEED, @kill_pct].min
|
|
60
|
+
elsif @item_count < @item_pct
|
|
61
|
+
@item_count = [@item_count + COUNT_SPEED, @item_pct].min
|
|
62
|
+
elsif @secret_count < @secret_pct
|
|
63
|
+
@secret_count = [@secret_count + COUNT_SPEED, @secret_pct].min
|
|
64
|
+
elsif @time_count < @time_secs
|
|
65
|
+
@time_count = [@time_count + 3, @time_secs].min
|
|
66
|
+
else
|
|
67
|
+
@counting_done = true
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def render(framebuffer)
|
|
72
|
+
# Background
|
|
73
|
+
draw_background(framebuffer)
|
|
74
|
+
|
|
75
|
+
# "Finished" text + level name
|
|
76
|
+
draw_sprite(framebuffer, @wifinish, 64, 4) if @wifinish
|
|
77
|
+
level_idx = map_to_level_index(@stats[:map])
|
|
78
|
+
lv = @level_names[level_idx]
|
|
79
|
+
draw_sprite(framebuffer, lv, (320 - (lv&.width || 0)) / 2, 24) if lv
|
|
80
|
+
|
|
81
|
+
# Kill, Item, Secret percentages
|
|
82
|
+
y = 60
|
|
83
|
+
draw_sprite(framebuffer, @wiostk, 50, y) if @wiostk
|
|
84
|
+
draw_percent(framebuffer, 260, y, @kill_count)
|
|
85
|
+
|
|
86
|
+
y += 24
|
|
87
|
+
draw_sprite(framebuffer, @wiosti, 50, y) if @wiosti
|
|
88
|
+
draw_percent(framebuffer, 260, y, @item_count)
|
|
89
|
+
|
|
90
|
+
y += 24
|
|
91
|
+
draw_sprite(framebuffer, @wiosts, 50, y) if @wiosts
|
|
92
|
+
draw_percent(framebuffer, 260, y, @secret_count)
|
|
93
|
+
|
|
94
|
+
# Time
|
|
95
|
+
y += 30
|
|
96
|
+
draw_sprite(framebuffer, @witime, 16, y) if @witime
|
|
97
|
+
draw_time(framebuffer, 160, y, @time_count)
|
|
98
|
+
|
|
99
|
+
# Par time
|
|
100
|
+
draw_sprite(framebuffer, @wipar, 176, y) if @wipar
|
|
101
|
+
draw_time(framebuffer, 292, y, @par_time)
|
|
102
|
+
|
|
103
|
+
# "Entering" next level (after counting done)
|
|
104
|
+
if @counting_done && @next_map
|
|
105
|
+
y += 30
|
|
106
|
+
draw_sprite(framebuffer, @wienter, 64, y) if @wienter
|
|
107
|
+
next_idx = map_to_level_index(@next_map)
|
|
108
|
+
nlv = @level_names[next_idx]
|
|
109
|
+
draw_sprite(framebuffer, nlv, (320 - (nlv&.width || 0)) / 2, y + 18) if nlv
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# "Press any key" hint after counting
|
|
113
|
+
if @counting_done && (@tic / 17) % 2 == 0
|
|
114
|
+
# Blink hint via skull
|
|
115
|
+
skull = @skulls[@tic / 8 % 2]
|
|
116
|
+
draw_sprite(framebuffer, skull, 144, 210) if skull
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def handle_key
|
|
121
|
+
if @counting_done
|
|
122
|
+
@finished = true
|
|
123
|
+
else
|
|
124
|
+
# Skip counting animation
|
|
125
|
+
@kill_count = @kill_pct
|
|
126
|
+
@item_count = @item_pct
|
|
127
|
+
@secret_count = @secret_pct
|
|
128
|
+
@time_count = @time_secs
|
|
129
|
+
@counting_done = true
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def map_to_level_index(map_name)
|
|
136
|
+
return 0 unless map_name
|
|
137
|
+
map_name[3].to_i - 1 # E1M1 -> 0, E1M2 -> 1, etc.
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def load_graphics
|
|
141
|
+
# Intermission number digits
|
|
142
|
+
@nums = (0..9).map { |n| load_patch("WINUM#{n}") }
|
|
143
|
+
@percent = load_patch('WIPCNT')
|
|
144
|
+
@colon = load_patch('WICOLON')
|
|
145
|
+
@minus = load_patch('WIMINUS')
|
|
146
|
+
|
|
147
|
+
# Labels
|
|
148
|
+
@wiostk = load_patch('WIOSTK') # "Kills"
|
|
149
|
+
@wiosti = load_patch('WIOSTI') # "Items"
|
|
150
|
+
@wiosts = load_patch('WIOSTS') # "Scrt" (Secrets)
|
|
151
|
+
@witime = load_patch('WITIME') # "Time"
|
|
152
|
+
@wipar = load_patch('WIPAR') # "Par"
|
|
153
|
+
@wifinish = load_patch('WIF') # "Finished"
|
|
154
|
+
@wienter = load_patch('WIENTER') # "Entering"
|
|
155
|
+
|
|
156
|
+
# Map background
|
|
157
|
+
@wimap = load_patch('WIMAP0')
|
|
158
|
+
|
|
159
|
+
# Level names (WILV00-WILV08)
|
|
160
|
+
@level_names = (0..8).map { |n| load_patch("WILV0#{n}") }
|
|
161
|
+
|
|
162
|
+
# Skull cursor
|
|
163
|
+
@skulls = [load_patch('M_SKULL1'), load_patch('M_SKULL2')]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def load_patch(name)
|
|
167
|
+
@gfx.send(:load_graphic, name)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def draw_background(framebuffer)
|
|
171
|
+
return unless @wimap
|
|
172
|
+
draw_fullscreen(framebuffer, @wimap)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def draw_fullscreen(framebuffer, sprite)
|
|
176
|
+
return unless sprite
|
|
177
|
+
y_offset = (240 - sprite.height) / 2
|
|
178
|
+
y_offset = [y_offset, 0].max
|
|
179
|
+
sprite.width.times do |x|
|
|
180
|
+
next if x >= 320
|
|
181
|
+
col = sprite.column_pixels(x)
|
|
182
|
+
next unless col
|
|
183
|
+
col.each_with_index do |color, y|
|
|
184
|
+
next unless color
|
|
185
|
+
sy = y + y_offset
|
|
186
|
+
next if sy < 0 || sy >= 240
|
|
187
|
+
framebuffer[sy * 320 + x] = color
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def draw_percent(framebuffer, right_x, y, value)
|
|
193
|
+
# Draw percent sign
|
|
194
|
+
draw_sprite(framebuffer, @percent, right_x, y) if @percent
|
|
195
|
+
|
|
196
|
+
# Draw number right-aligned before percent
|
|
197
|
+
draw_num_right(framebuffer, right_x - 2, y, value)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def draw_time(framebuffer, right_x, y, seconds)
|
|
201
|
+
mins = seconds / 60
|
|
202
|
+
secs = seconds % 60
|
|
203
|
+
|
|
204
|
+
# Draw seconds (2 digits, zero-padded)
|
|
205
|
+
draw_num_right(framebuffer, right_x, y, secs, pad: 2)
|
|
206
|
+
|
|
207
|
+
# Colon
|
|
208
|
+
colon_x = right_x - num_width * 2 - 4
|
|
209
|
+
draw_sprite(framebuffer, @colon, colon_x, y) if @colon
|
|
210
|
+
|
|
211
|
+
# Minutes
|
|
212
|
+
draw_num_right(framebuffer, colon_x - 2, y, mins)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def num_width
|
|
216
|
+
@nums[0]&.width || 14
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def draw_num_right(framebuffer, right_x, y, value, pad: 0)
|
|
220
|
+
w = num_width
|
|
221
|
+
str = value.to_i.to_s
|
|
222
|
+
str = str.rjust(pad, '0') if pad > 0
|
|
223
|
+
x = right_x
|
|
224
|
+
str.reverse.each_char do |ch|
|
|
225
|
+
x -= w
|
|
226
|
+
digit = @nums[ch.to_i]
|
|
227
|
+
draw_sprite(framebuffer, digit, x, y) if digit
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def draw_sprite(framebuffer, sprite, x, y)
|
|
232
|
+
return unless sprite
|
|
233
|
+
sprite.width.times do |col_x|
|
|
234
|
+
sx = x + col_x
|
|
235
|
+
next if sx < 0 || sx >= 320
|
|
236
|
+
col = sprite.column_pixels(col_x)
|
|
237
|
+
next unless col
|
|
238
|
+
col.each_with_index do |color, col_y|
|
|
239
|
+
next unless color
|
|
240
|
+
sy = y + col_y
|
|
241
|
+
next if sy < 0 || sy >= 240
|
|
242
|
+
framebuffer[sy * 320 + sx] = color
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|