doom 0.8.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 +134 -24
- data/lib/doom/benchmark.rb +282 -0
- data/lib/doom/game/animations.rb +9 -2
- data/lib/doom/game/combat.rb +239 -153
- data/lib/doom/game/framebuffer_blitter.rb +38 -0
- data/lib/doom/game/geometry.rb +68 -0
- data/lib/doom/game/intermission.rb +22 -39
- data/lib/doom/game/item_pickup.rb +87 -75
- data/lib/doom/game/menu.rb +90 -85
- data/lib/doom/game/monster_ai.rb +150 -136
- data/lib/doom/game/player.rb +105 -0
- data/lib/doom/game/player_physics.rb +384 -0
- data/lib/doom/game/player_state.rb +38 -64
- data/lib/doom/game/random.rb +82 -0
- data/lib/doom/game/sector_actions.rb +169 -93
- data/lib/doom/game/sector_effects.rb +25 -18
- data/lib/doom/game/snapshot.rb +584 -0
- data/lib/doom/game/sound_engine.rb +33 -58
- 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 +604 -832
- data/lib/doom/platform/sdl.rb +74 -0
- data/lib/doom/platform/window_logic.rb +61 -0
- data/lib/doom/render/font.rb +5 -3
- 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 +466 -440
- data/lib/doom/render/renderer_factory.rb +50 -0
- data/lib/doom/render/screen_melt.rb +7 -7
- 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 +9 -10
- 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 +12 -15
- data/lib/doom/wad/sound.rb +14 -6
- data/lib/doom/wad/sprite.rb +36 -35
- data/lib/doom/wad/texture.rb +5 -5
- data/lib/doom/wad_downloader.rb +38 -56
- data/lib/doom.rb +197 -16
- metadata +94 -7
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Game
|
|
5
|
+
# Player movement and vertical physics, extracted from the Gosu window
|
|
6
|
+
# so it can be unit-tested without a graphics stack.
|
|
7
|
+
#
|
|
8
|
+
# Tracks the player's feet position (floor_z) and vertical velocity (momz)
|
|
9
|
+
# in DOOM map units, matching Chocolate Doom's P_ZMovement semantics:
|
|
10
|
+
# GRAVITY=1 unit/tic^2, falls clamp to floor, viewheight squat on landing.
|
|
11
|
+
class PlayerPhysics
|
|
12
|
+
PLAYER_RADIUS = 16.0
|
|
13
|
+
|
|
14
|
+
MAX_STEP_UP = 24
|
|
15
|
+
MIN_HEADROOM = 56
|
|
16
|
+
|
|
17
|
+
GRAVITY = 1.0
|
|
18
|
+
INITIAL_FALL_MOMZ = -2.0 # First-tic kick when momz==0 (P_ZMovement uses -GRAVITY*2)
|
|
19
|
+
FALL_IMPACT_THRESHOLD = -8.0 # momz < -GRAVITY*8 triggers viewheight squat
|
|
20
|
+
|
|
21
|
+
TICRATE = 35.0
|
|
22
|
+
|
|
23
|
+
# Horizontal movement runs per-tic, not per-frame. A frame-rate-dependent
|
|
24
|
+
# integration can never be deterministic, and lockstep needs every peer to
|
|
25
|
+
# get the same result from the same ticcmd.
|
|
26
|
+
#
|
|
27
|
+
# FRICTION is DOOM's exact per-tic value (ORIG_FRICTION 0xE800/65536).
|
|
28
|
+
# Momentum is carried in units/sec, so THRUST_PER_TIC is chosen to hold
|
|
29
|
+
# the same terminal speed the old continuous model had:
|
|
30
|
+
# v_terminal = thrust * FRICTION / (1 - FRICTION)
|
|
31
|
+
FRICTION = 0.90625
|
|
32
|
+
TERMINAL_SPEED = 264.0 # units/sec
|
|
33
|
+
THRUST_PER_TIC = TERMINAL_SPEED * (1.0 - FRICTION) / FRICTION # ~27.3
|
|
34
|
+
SIDE_THRUST_SCALE = 0.8 # DOOM strafes slower than it walks
|
|
35
|
+
STOPSPEED = 0.5 # Snap-to-zero threshold (units/sec)
|
|
36
|
+
|
|
37
|
+
# Solid thing types with their collision radii (from mobjinfo[] MF_SOLID).
|
|
38
|
+
SOLID_THING_RADIUS = {
|
|
39
|
+
9 => 20, 65 => 20, 66 => 20, 67 => 20, 68 => 20, # Shotgun Guy variants
|
|
40
|
+
3004 => 20, 84 => 20, # Zombieman
|
|
41
|
+
3001 => 20, # Imp
|
|
42
|
+
3002 => 30, 58 => 30, # Demon, Spectre
|
|
43
|
+
3003 => 24, 69 => 24, # Baron, Hell Knight
|
|
44
|
+
3006 => 16, # Lost Soul
|
|
45
|
+
3005 => 31, # Cacodemon
|
|
46
|
+
16 => 40, # Cyberdemon
|
|
47
|
+
7 => 128, # Spider Mastermind
|
|
48
|
+
64 => 20, # Archvile
|
|
49
|
+
71 => 31, # Pain Elemental
|
|
50
|
+
2035 => 10, # Barrel
|
|
51
|
+
2028 => 16, # Tall lamp
|
|
52
|
+
48 => 16, 30 => 16, 32 => 16, # Tech column, green/red pillars
|
|
53
|
+
31 => 16, 33 => 16, 36 => 16, # Short pillars
|
|
54
|
+
41 => 16, 43 => 16, # Evil eye, burnt tree
|
|
55
|
+
54 => 32, # Brown tree
|
|
56
|
+
44 => 16, 45 => 16, 46 => 16, # Tall torches
|
|
57
|
+
55 => 16, 56 => 16, 57 => 16, # Short torches
|
|
58
|
+
47 => 16, 70 => 16, # Stubs
|
|
59
|
+
85 => 16, 86 => 16, # Tall tech lamps
|
|
60
|
+
2046 => 16 # Burning barrel
|
|
61
|
+
}.freeze
|
|
62
|
+
|
|
63
|
+
attr_reader :floor_z, :momz
|
|
64
|
+
attr_writer :skill_hidden, :item_pickup, :combat
|
|
65
|
+
|
|
66
|
+
def initialize(map, player_state)
|
|
67
|
+
@map = map
|
|
68
|
+
@player_state = player_state
|
|
69
|
+
@floor_z = nil
|
|
70
|
+
@momz = 0.0
|
|
71
|
+
@skill_hidden = {}
|
|
72
|
+
@item_pickup = nil
|
|
73
|
+
@combat = nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def reset
|
|
77
|
+
@floor_z = nil
|
|
78
|
+
@momz = 0.0
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Eye position used by the renderer: feet + viewheight + view-bob.
|
|
82
|
+
# Returns nil before the first settle_at.
|
|
83
|
+
def eye_z
|
|
84
|
+
return nil unless @floor_z
|
|
85
|
+
|
|
86
|
+
if @player_state
|
|
87
|
+
@floor_z + @player_state.viewheight + @player_state.view_bob_offset
|
|
88
|
+
else
|
|
89
|
+
@floor_z + PlayerState::VIEWHEIGHT
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Bring the player onto the floor at (x, y) after a horizontal move.
|
|
94
|
+
# Snaps up for step-up (gated to <= MAX_STEP_UP by valid_move?), leaves
|
|
95
|
+
# @floor_z alone for step-down so per-tic gravity can drop the player.
|
|
96
|
+
def settle_at(x, y)
|
|
97
|
+
sector = @map.sector_at(x, y)
|
|
98
|
+
return unless sector
|
|
99
|
+
|
|
100
|
+
new_floor = sector.floor_height
|
|
101
|
+
|
|
102
|
+
if @player_state
|
|
103
|
+
@floor_z ||= new_floor
|
|
104
|
+
if new_floor > @floor_z
|
|
105
|
+
step = new_floor - @floor_z
|
|
106
|
+
@player_state.notify_step(step) if step.abs <= MAX_STEP_UP
|
|
107
|
+
@floor_z = new_floor
|
|
108
|
+
@momz = 0.0
|
|
109
|
+
end
|
|
110
|
+
else
|
|
111
|
+
@floor_z = new_floor
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Per-tic vertical movement. Mutates @floor_z and @momz; calls
|
|
116
|
+
# player_state.notify_step / apply_fall_impact as needed.
|
|
117
|
+
def step(x, y)
|
|
118
|
+
return unless @player_state && @floor_z
|
|
119
|
+
|
|
120
|
+
sector = @map.sector_at(x, y)
|
|
121
|
+
return unless sector
|
|
122
|
+
|
|
123
|
+
ground = sector.floor_height
|
|
124
|
+
|
|
125
|
+
if @floor_z > ground
|
|
126
|
+
@momz = (@momz == 0.0 ? INITIAL_FALL_MOMZ : @momz - GRAVITY)
|
|
127
|
+
@floor_z += @momz
|
|
128
|
+
if @floor_z <= ground
|
|
129
|
+
impact_momz = @momz
|
|
130
|
+
@floor_z = ground
|
|
131
|
+
@momz = 0.0
|
|
132
|
+
@player_state.apply_fall_impact(impact_momz) if impact_momz < FALL_IMPACT_THRESHOLD
|
|
133
|
+
end
|
|
134
|
+
elsif @floor_z < ground
|
|
135
|
+
# Floor rose under player (lift, raising sector)
|
|
136
|
+
step_amount = ground - @floor_z
|
|
137
|
+
@player_state.notify_step(step_amount) if step_amount.abs <= MAX_STEP_UP
|
|
138
|
+
@floor_z = ground
|
|
139
|
+
@momz = 0.0
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Advance one player by one tic from its ticcmd: turn, thrust, friction,
|
|
144
|
+
# then move with wall sliding. This is the whole horizontal simulation,
|
|
145
|
+
# and it is a pure function of (player, cmd, map) -- no wall clock, no
|
|
146
|
+
# frame rate, no Kernel#rand. That is what makes lockstep possible.
|
|
147
|
+
def run_tic(player, cmd)
|
|
148
|
+
player.turn(cmd.angleturn) if cmd.angleturn != 0.0
|
|
149
|
+
|
|
150
|
+
if cmd.forwardmove != 0.0
|
|
151
|
+
thrust = cmd.forwardmove * THRUST_PER_TIC
|
|
152
|
+
player.momx += player.cos_angle * thrust
|
|
153
|
+
player.momy += player.sin_angle * thrust
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
if cmd.sidemove != 0.0
|
|
157
|
+
thrust = cmd.sidemove * THRUST_PER_TIC * SIDE_THRUST_SCALE
|
|
158
|
+
# Strafe right is 90 degrees clockwise from facing.
|
|
159
|
+
player.momx += player.sin_angle * thrust
|
|
160
|
+
player.momy -= player.cos_angle * thrust
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
if !cmd.moving? && player.momx.abs < STOPSPEED && player.momy.abs < STOPSPEED
|
|
164
|
+
player.momx = 0.0
|
|
165
|
+
player.momy = 0.0
|
|
166
|
+
else
|
|
167
|
+
player.momx *= FRICTION
|
|
168
|
+
player.momy *= FRICTION
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
return unless player.momx.abs > STOPSPEED || player.momy.abs > STOPSPEED
|
|
172
|
+
|
|
173
|
+
move(player, player.momx / TICRATE, player.momy / TICRATE)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Move a player by (dx, dy) with collision and wall sliding, settling it
|
|
177
|
+
# onto the floor wherever it ends up.
|
|
178
|
+
def move(player, dx, dy)
|
|
179
|
+
old_x = player.x
|
|
180
|
+
old_y = player.y
|
|
181
|
+
new_x = old_x + dx
|
|
182
|
+
new_y = old_y + dy
|
|
183
|
+
|
|
184
|
+
if valid_move?(old_x, old_y, new_x, new_y)
|
|
185
|
+
player.move_to(new_x, new_y)
|
|
186
|
+
settle(player, new_x, new_y)
|
|
187
|
+
return
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Wall sliding: project movement along the blocking wall
|
|
191
|
+
slide_x, slide_y = compute_slide(old_x, old_y, dx, dy)
|
|
192
|
+
if slide_x && (slide_x != 0.0 || slide_y != 0.0)
|
|
193
|
+
sx = old_x + slide_x
|
|
194
|
+
sy = old_y + slide_y
|
|
195
|
+
if valid_move?(old_x, old_y, sx, sy)
|
|
196
|
+
player.move_to(sx, sy)
|
|
197
|
+
settle(player, sx, sy)
|
|
198
|
+
scale = [dx.abs, dy.abs].max.nonzero? || 1
|
|
199
|
+
player.momx = slide_x / scale * player.momx.abs
|
|
200
|
+
player.momy = slide_y / scale * player.momy.abs
|
|
201
|
+
return
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Fallback: try axis-aligned sliding
|
|
206
|
+
if dx != 0.0 && valid_move?(old_x, old_y, new_x, old_y)
|
|
207
|
+
player.move_to(new_x, old_y)
|
|
208
|
+
settle(player, new_x, old_y)
|
|
209
|
+
player.momy = 0.0
|
|
210
|
+
elsif dy != 0.0 && valid_move?(old_x, old_y, old_x, new_y)
|
|
211
|
+
player.move_to(old_x, new_y)
|
|
212
|
+
settle(player, old_x, new_y)
|
|
213
|
+
player.momx = 0.0
|
|
214
|
+
else
|
|
215
|
+
player.momx = 0.0
|
|
216
|
+
player.momy = 0.0
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Settle onto the floor and push the resulting eye height to the player.
|
|
221
|
+
def settle(player, x, y)
|
|
222
|
+
settle_at(x, y)
|
|
223
|
+
z = eye_z
|
|
224
|
+
player.z = z if z
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# When valid_move? fails, find the nearest blocking wall and return a
|
|
228
|
+
# slide vector projected along it. Returns nil if no wall blocks.
|
|
229
|
+
def compute_slide(px, py, dx, dy)
|
|
230
|
+
best_wall = nil
|
|
231
|
+
best_dist = Float::INFINITY
|
|
232
|
+
|
|
233
|
+
each_nearby_linedef(px, py, px + dx, py + dy) do |linedef|
|
|
234
|
+
v1 = @map.vertices[linedef.v1]
|
|
235
|
+
v2 = @map.vertices[linedef.v2]
|
|
236
|
+
|
|
237
|
+
next unless Geometry.line_circle_intersect?(v1.x, v1.y, v2.x, v2.y, px + dx, py + dy, PLAYER_RADIUS)
|
|
238
|
+
next unless linedef_blocks?(linedef, px, py, px + dx, py + dy) ||
|
|
239
|
+
crosses_blocking_linedef?(px, py, px + dx, py + dy, linedef)
|
|
240
|
+
|
|
241
|
+
dist = Geometry.point_to_segment_distance(px, py, v1.x, v1.y, v2.x, v2.y)
|
|
242
|
+
if dist < best_dist
|
|
243
|
+
best_dist = dist
|
|
244
|
+
best_wall = linedef
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
return nil unless best_wall
|
|
249
|
+
|
|
250
|
+
v1 = @map.vertices[best_wall.v1]
|
|
251
|
+
v2 = @map.vertices[best_wall.v2]
|
|
252
|
+
wall_dx = (v2.x - v1.x).to_f
|
|
253
|
+
wall_dy = (v2.y - v1.y).to_f
|
|
254
|
+
wall_len = Math.sqrt((wall_dx * wall_dx) + (wall_dy * wall_dy))
|
|
255
|
+
return nil if wall_len == 0
|
|
256
|
+
|
|
257
|
+
wall_dx /= wall_len
|
|
258
|
+
wall_dy /= wall_len
|
|
259
|
+
dot = (dx * wall_dx) + (dy * wall_dy)
|
|
260
|
+
[dot * wall_dx, dot * wall_dy]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# True if the player can occupy (new_x, new_y). Step-up is capped at
|
|
264
|
+
# MAX_STEP_UP from the player's current floor; step-down is unlimited.
|
|
265
|
+
def valid_move?(old_x, old_y, new_x, new_y)
|
|
266
|
+
sector = @map.sector_at(new_x, new_y)
|
|
267
|
+
return false unless sector
|
|
268
|
+
|
|
269
|
+
current_floor = @floor_z || sector.floor_height
|
|
270
|
+
return false if sector.floor_height - current_floor > MAX_STEP_UP
|
|
271
|
+
|
|
272
|
+
each_nearby_linedef(old_x, old_y, new_x, new_y) do |linedef|
|
|
273
|
+
return false if linedef_blocks?(linedef, old_x, old_y, new_x, new_y)
|
|
274
|
+
return false if crosses_blocking_linedef?(old_x, old_y, new_x, new_y, linedef)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
return false if collides_with_solid_thing?(new_x, new_y)
|
|
278
|
+
|
|
279
|
+
true
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
private
|
|
283
|
+
|
|
284
|
+
# Iterate linedefs that may interact with the player moving from
|
|
285
|
+
# (x1, y1) to (x2, y2). Uses BLOCKMAP when available; otherwise falls
|
|
286
|
+
# back to scanning all linedefs.
|
|
287
|
+
def each_nearby_linedef(x1, y1, x2, y2, &)
|
|
288
|
+
unless @map.respond_to?(:blockmap_loaded?) && @map.blockmap_loaded?
|
|
289
|
+
@map.linedefs.each(&)
|
|
290
|
+
return
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Pad the bbox by player radius so walls just outside still register.
|
|
294
|
+
min_x = [x1, x2].min - PLAYER_RADIUS
|
|
295
|
+
max_x = [x1, x2].max + PLAYER_RADIUS
|
|
296
|
+
min_y = [y1, y2].min - PLAYER_RADIUS
|
|
297
|
+
max_y = [y1, y2].max + PLAYER_RADIUS
|
|
298
|
+
@map.each_linedef_near(min_x, min_y, max_x, max_y, &)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def crosses_blocking_linedef?(x1, y1, x2, y2, linedef)
|
|
302
|
+
v1 = @map.vertices[linedef.v1]
|
|
303
|
+
v2 = @map.vertices[linedef.v2]
|
|
304
|
+
|
|
305
|
+
return Geometry.segments_intersect?(x1, y1, x2, y2, v1.x, v1.y, v2.x, v2.y) if linedef.sidedef_left == 0xFFFF
|
|
306
|
+
|
|
307
|
+
if (linedef.flags & 0x0001) != 0 # ML_BLOCKING
|
|
308
|
+
return Geometry.segments_intersect?(x1, y1, x2, y2, v1.x, v1.y, v2.x, v2.y)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
front_sector = sector_for_side(linedef.sidedef_right)
|
|
312
|
+
back_sector = sector_for_side(linedef.sidedef_left)
|
|
313
|
+
|
|
314
|
+
min_ceiling = [front_sector.ceiling_height, back_sector.ceiling_height].min
|
|
315
|
+
max_floor = [front_sector.floor_height, back_sector.floor_height].max
|
|
316
|
+
current_floor = @floor_z || front_sector.floor_height
|
|
317
|
+
step_up = max_floor - current_floor
|
|
318
|
+
|
|
319
|
+
return false if step_up <= MAX_STEP_UP && (min_ceiling - max_floor) >= MIN_HEADROOM
|
|
320
|
+
|
|
321
|
+
Geometry.segments_intersect?(x1, y1, x2, y2, v1.x, v1.y, v2.x, v2.y)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def linedef_blocks?(linedef, old_x, old_y, new_x, new_y)
|
|
325
|
+
v1 = @map.vertices[linedef.v1]
|
|
326
|
+
v2 = @map.vertices[linedef.v2]
|
|
327
|
+
|
|
328
|
+
return false unless Geometry.line_circle_intersect?(v1.x, v1.y, v2.x, v2.y, new_x, new_y, PLAYER_RADIUS)
|
|
329
|
+
|
|
330
|
+
impassable =
|
|
331
|
+
if linedef.sidedef_left == 0xFFFF
|
|
332
|
+
true
|
|
333
|
+
else
|
|
334
|
+
# ML_BLOCKING on two-sided lines is handled by crosses_blocking_linedef?
|
|
335
|
+
# (a proximity check would block the player when standing near, not
|
|
336
|
+
# just crossing).
|
|
337
|
+
front_sector = sector_for_side(linedef.sidedef_right)
|
|
338
|
+
back_sector = sector_for_side(linedef.sidedef_left)
|
|
339
|
+
|
|
340
|
+
min_ceiling = [front_sector.ceiling_height, back_sector.ceiling_height].min
|
|
341
|
+
max_floor = [front_sector.floor_height, back_sector.floor_height].max
|
|
342
|
+
current_floor = @floor_z || front_sector.floor_height
|
|
343
|
+
step_up = max_floor - current_floor
|
|
344
|
+
|
|
345
|
+
step_up > MAX_STEP_UP || (min_ceiling - max_floor) < MIN_HEADROOM
|
|
346
|
+
end
|
|
347
|
+
return false unless impassable
|
|
348
|
+
|
|
349
|
+
# The circle overlaps an impassable wall at the destination. Block the
|
|
350
|
+
# move only if it does not increase the distance to that wall, so a body
|
|
351
|
+
# that is already overlapping one -- e.g. after landing from a fall at
|
|
352
|
+
# the base of a tall step, where the airborne slide let the radius graze
|
|
353
|
+
# the ledge -- can still move away and unstick, but can never push
|
|
354
|
+
# further in or climb the step. A normal head-on approach still stops at
|
|
355
|
+
# radius; sliding parallel (equal distance) is still allowed.
|
|
356
|
+
d_new = Geometry.point_to_segment_distance(new_x, new_y, v1.x, v1.y, v2.x, v2.y)
|
|
357
|
+
d_old = Geometry.point_to_segment_distance(old_x, old_y, v1.x, v1.y, v2.x, v2.y)
|
|
358
|
+
d_new < d_old
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def collides_with_solid_thing?(x, y)
|
|
362
|
+
picked = @item_pickup&.picked_up
|
|
363
|
+
@map.things.each_with_index do |thing, idx|
|
|
364
|
+
next if @skill_hidden[idx]
|
|
365
|
+
next if picked && picked[idx]
|
|
366
|
+
next if @combat && @combat.dead?(idx)
|
|
367
|
+
|
|
368
|
+
thing_radius = SOLID_THING_RADIUS[thing.type]
|
|
369
|
+
next unless thing_radius
|
|
370
|
+
|
|
371
|
+
dx = x - thing.x
|
|
372
|
+
dy = y - thing.y
|
|
373
|
+
min_dist = PLAYER_RADIUS + thing_radius
|
|
374
|
+
return true if (dx * dx) + (dy * dy) < min_dist * min_dist
|
|
375
|
+
end
|
|
376
|
+
false
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def sector_for_side(side_idx)
|
|
380
|
+
@map.sectors[@map.sidedefs[side_idx].sector]
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
@@ -36,25 +36,14 @@ module Doom
|
|
|
36
36
|
WEAPON_PLASMA => 8, # Fast energy weapon
|
|
37
37
|
WEAPON_BFG => 60, # Long charge + fire
|
|
38
38
|
WEAPON_CHAINSAW => 6 # Fast melee
|
|
39
|
-
}.freeze
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
attr_accessor :ammo_bullets, :ammo_shells, :ammo_rockets, :ammo_cells
|
|
43
|
-
attr_accessor :max_bullets, :max_shells, :max_rockets, :max_cells
|
|
44
|
-
attr_accessor :weapon, :has_weapons
|
|
45
|
-
attr_accessor :keys
|
|
46
|
-
attr_accessor :attacking, :attack_frame, :attack_tics
|
|
47
|
-
attr_accessor :bob_angle, :bob_amount
|
|
48
|
-
attr_accessor :is_moving
|
|
49
|
-
attr_accessor :dead, :death_tic
|
|
50
|
-
attr_accessor :damage_count # Red flash intensity (0-8), decays each tic
|
|
51
|
-
attr_accessor :god_mode, :infinite_ammo
|
|
39
|
+
}.freeze # Red flash intensity (0-8), decays each tic
|
|
40
|
+
attr_accessor :health, :armor, :max_health, :max_armor, :ammo_bullets, :ammo_shells, :ammo_rockets, :ammo_cells,
|
|
41
|
+
:max_bullets, :max_shells, :max_rockets, :max_cells, :weapon, :has_weapons, :keys, :attacking, :attack_frame, :attack_tics, :bob_angle, :bob_amount, :is_moving, :dead, :death_tic, :damage_count, :god_mode, :infinite_ammo
|
|
52
42
|
|
|
53
43
|
# Smooth step-up/down (matching Chocolate Doom's P_CalcHeight / P_ZMovement)
|
|
54
44
|
VIEWHEIGHT = 41.0
|
|
55
45
|
VIEWHEIGHT_HALF = VIEWHEIGHT / 2.0
|
|
56
|
-
DELTA_ACCEL = 0.25
|
|
57
|
-
attr_reader :viewheight, :deltaviewheight
|
|
46
|
+
DELTA_ACCEL = 0.25 # deltaviewheight += FRACUNIT/4 per tic
|
|
58
47
|
|
|
59
48
|
# View bob (camera bounce when walking, matching Chocolate Doom's
|
|
60
49
|
# P_CalcHeight + P_XYMovement + P_Thrust from p_user.c / p_mobj.c)
|
|
@@ -68,7 +57,7 @@ module Doom
|
|
|
68
57
|
BOB_DECAY_RATE = 3.44 # Friction as continuous decay rate (1/sec)
|
|
69
58
|
BOB_THRUST = 26.0 # Walk thrust (map units/sec), gives terminal ~7.5
|
|
70
59
|
BOB_FREQUENCY = 11.0 # Bob cycle frequency (rad/sec): FINEANGLES/20 * 35 / 8192 * 2*PI
|
|
71
|
-
attr_reader :view_bob_offset
|
|
60
|
+
attr_reader :viewheight, :deltaviewheight, :view_bob_offset
|
|
72
61
|
|
|
73
62
|
def initialize
|
|
74
63
|
reset
|
|
@@ -132,7 +121,10 @@ module Doom
|
|
|
132
121
|
@view_bob_offset = 0.0
|
|
133
122
|
@momx = 0.0 # Simulated X momentum (map units/sec, not actual movement)
|
|
134
123
|
@momy = 0.0 # Simulated Y momentum
|
|
135
|
-
|
|
124
|
+
# Held at zero and never read by game logic, but part of the snapshot
|
|
125
|
+
# wire format and state fingerprint (PLAYER_STATE_FLOATS), so removing
|
|
126
|
+
# them would need a snapshot version bump. Left in place deliberately.
|
|
127
|
+
@thrust_x = 0.0
|
|
136
128
|
@thrust_y = 0.0
|
|
137
129
|
@view_bob_angle = 0.0
|
|
138
130
|
end
|
|
@@ -156,21 +148,6 @@ module Doom
|
|
|
156
148
|
end
|
|
157
149
|
end
|
|
158
150
|
|
|
159
|
-
def max_ammo_for_weapon
|
|
160
|
-
case @weapon
|
|
161
|
-
when WEAPON_PISTOL, WEAPON_CHAINGUN
|
|
162
|
-
@max_bullets
|
|
163
|
-
when WEAPON_SHOTGUN
|
|
164
|
-
@max_shells
|
|
165
|
-
when WEAPON_ROCKET
|
|
166
|
-
@max_rockets
|
|
167
|
-
when WEAPON_PLASMA, WEAPON_BFG
|
|
168
|
-
@max_cells
|
|
169
|
-
else
|
|
170
|
-
nil
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
|
|
174
151
|
def can_attack?
|
|
175
152
|
return true if @weapon == WEAPON_FIST || @weapon == WEAPON_CHAINSAW
|
|
176
153
|
return true if @infinite_ammo
|
|
@@ -219,21 +196,21 @@ module Doom
|
|
|
219
196
|
@attack_frame = (@attack_tics / tics_per_frame).to_i
|
|
220
197
|
|
|
221
198
|
# Attack finished?
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
199
|
+
return unless @attack_tics >= duration
|
|
200
|
+
|
|
201
|
+
@attacking = false
|
|
202
|
+
@attack_frame = 0
|
|
203
|
+
@attack_tics = 0
|
|
227
204
|
end
|
|
228
205
|
|
|
229
206
|
def update_bob(delta_time)
|
|
230
207
|
if @is_moving
|
|
231
208
|
# Increase bob while moving
|
|
232
209
|
@bob_angle += delta_time * 10.0
|
|
233
|
-
@bob_amount = [@bob_amount + delta_time * 16.0, 6.0].min
|
|
210
|
+
@bob_amount = [@bob_amount + (delta_time * 16.0), 6.0].min
|
|
234
211
|
else
|
|
235
212
|
# Decay bob when stopped
|
|
236
|
-
@bob_amount = [@bob_amount - delta_time * 12.0, 0.0].max
|
|
213
|
+
@bob_amount = [@bob_amount - (delta_time * 12.0), 0.0].max
|
|
237
214
|
end
|
|
238
215
|
end
|
|
239
216
|
|
|
@@ -242,10 +219,18 @@ module Doom
|
|
|
242
219
|
# so the camera doesn't snap, then let P_CalcHeight recover it smoothly.
|
|
243
220
|
def notify_step(step_amount)
|
|
244
221
|
return if step_amount == 0
|
|
222
|
+
|
|
245
223
|
@viewheight -= step_amount
|
|
246
224
|
@deltaviewheight = (VIEWHEIGHT - @viewheight) / 8.0
|
|
247
225
|
end
|
|
248
226
|
|
|
227
|
+
# Called on landing after a fall. Matches Chocolate Doom P_ZMovement:
|
|
228
|
+
# deltaviewheight = momz >> 3, producing a squat that update_viewheight
|
|
229
|
+
# then recovers via DELTA_ACCEL.
|
|
230
|
+
def apply_fall_impact(momz)
|
|
231
|
+
@deltaviewheight = momz / 8.0
|
|
232
|
+
end
|
|
233
|
+
|
|
249
234
|
# Gradually restore viewheight to VIEWHEIGHT (called each tic).
|
|
250
235
|
# Matches Chocolate Doom P_CalcHeight viewheight recovery loop.
|
|
251
236
|
# For step-up: viewheight < 41, delta > 0, accelerates upward.
|
|
@@ -263,10 +248,10 @@ module Doom
|
|
|
263
248
|
@deltaviewheight = 1.0 if @deltaviewheight <= 0
|
|
264
249
|
end
|
|
265
250
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
251
|
+
return unless @deltaviewheight != 0
|
|
252
|
+
|
|
253
|
+
@deltaviewheight += DELTA_ACCEL
|
|
254
|
+
@deltaviewheight = 0.0 if @deltaviewheight.abs < 0.01 && (@viewheight - VIEWHEIGHT).abs < 0.5
|
|
270
255
|
end
|
|
271
256
|
|
|
272
257
|
# Set movement momentum directly (called from GosuWindow with actual
|
|
@@ -281,13 +266,13 @@ module Doom
|
|
|
281
266
|
# bob = (momx*momx + momy*momy) >> 2, capped at MAXBOB
|
|
282
267
|
# viewz += finesine[angle] * bob/2
|
|
283
268
|
# Momentum is in units/sec; DOOM's bob uses units/tic (divide by 35).
|
|
284
|
-
BOB_MOM_SCALE = 1.0 / (35.0 * 35.0 * 4.0)
|
|
269
|
+
BOB_MOM_SCALE = 1.0 / (35.0 * 35.0 * 4.0) # (mom/35)^2 / 4
|
|
285
270
|
|
|
286
271
|
def update_view_bob(delta_time)
|
|
287
272
|
dt = delta_time.clamp(0.001, 0.05)
|
|
288
273
|
|
|
289
274
|
# P_CalcHeight: bob = (momx_per_tic^2 + momy_per_tic^2) / 4, capped at MAXBOB
|
|
290
|
-
bob = (@momx * @momx + @momy * @momy) * BOB_MOM_SCALE
|
|
275
|
+
bob = ((@momx * @momx) + (@momy * @momy)) * BOB_MOM_SCALE
|
|
291
276
|
bob = MAXBOB if bob > MAXBOB
|
|
292
277
|
|
|
293
278
|
# Advance bob sine wave (FINEANGLES/20 per tic = ~11 rad/sec)
|
|
@@ -305,17 +290,6 @@ module Doom
|
|
|
305
290
|
Math.sin(@bob_angle * 2) * @bob_amount * 0.5
|
|
306
291
|
end
|
|
307
292
|
|
|
308
|
-
def health_level
|
|
309
|
-
# 0 = dying, 4 = full health
|
|
310
|
-
case @health
|
|
311
|
-
when 80..200 then 4
|
|
312
|
-
when 60..79 then 3
|
|
313
|
-
when 40..59 then 2
|
|
314
|
-
when 20..39 then 1
|
|
315
|
-
else 0
|
|
316
|
-
end
|
|
317
|
-
end
|
|
318
|
-
|
|
319
293
|
def switch_weapon(weapon_num)
|
|
320
294
|
return unless weapon_num >= 0 && weapon_num < 8
|
|
321
295
|
return unless @has_weapons[weapon_num]
|
|
@@ -331,7 +305,7 @@ module Doom
|
|
|
331
305
|
|
|
332
306
|
absorbed = 0
|
|
333
307
|
if @armor > 0
|
|
334
|
-
absorbed = amount / 3
|
|
308
|
+
absorbed = amount / 3 # Green armor absorbs 1/3
|
|
335
309
|
absorbed = @armor if absorbed > @armor
|
|
336
310
|
@armor -= absorbed
|
|
337
311
|
end
|
|
@@ -340,13 +314,13 @@ module Doom
|
|
|
340
314
|
@health -= actual
|
|
341
315
|
|
|
342
316
|
# Red flash proportional to damage (capped at palette 8)
|
|
343
|
-
@damage_count = [(@damage_count + actual / 2.0).ceil, 8].min
|
|
317
|
+
@damage_count = [(@damage_count + (actual / 2.0)).ceil, 8].min
|
|
344
318
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
319
|
+
return unless @health <= 0
|
|
320
|
+
|
|
321
|
+
@health = 0
|
|
322
|
+
@damage_count = 8
|
|
323
|
+
die
|
|
350
324
|
end
|
|
351
325
|
|
|
352
326
|
# Decay damage flash each tic
|
|
@@ -358,7 +332,7 @@ module Doom
|
|
|
358
332
|
@dead = true
|
|
359
333
|
@death_tic = 0
|
|
360
334
|
@attacking = false
|
|
361
|
-
@deltaviewheight = -VIEWHEIGHT / 8.0
|
|
335
|
+
@deltaviewheight = -VIEWHEIGHT / 8.0 # View drops to ground
|
|
362
336
|
end
|
|
363
337
|
end
|
|
364
338
|
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Game
|
|
5
|
+
# DOOM's P_Random -- a fixed 256-entry table walked by an index, not a PRNG.
|
|
6
|
+
#
|
|
7
|
+
# The point is determinism: two peers running the same tics with the same
|
|
8
|
+
# starting index observe identical results, which is what lockstep
|
|
9
|
+
# networking requires. Kernel#rand cannot give us that.
|
|
10
|
+
#
|
|
11
|
+
# Matches m_random.c. Only the game-state RNG lives here; presentation-only
|
|
12
|
+
# randomness (screen melt) stays on Kernel#rand since it never feeds the
|
|
13
|
+
# simulation.
|
|
14
|
+
class Random
|
|
15
|
+
RNDTABLE = [
|
|
16
|
+
0, 8, 109, 220, 222, 241, 149, 107, 75, 248, 254, 140, 16, 66,
|
|
17
|
+
74, 21, 211, 47, 80, 242, 154, 27, 205, 128, 161, 89, 77, 36,
|
|
18
|
+
95, 110, 85, 48, 212, 140, 211, 249, 22, 79, 200, 50, 28, 188,
|
|
19
|
+
52, 140, 202, 120, 68, 145, 62, 70, 184, 190, 91, 197, 152, 224,
|
|
20
|
+
149, 104, 25, 178, 252, 182, 202, 182, 141, 197, 4, 81, 181, 242,
|
|
21
|
+
145, 42, 39, 227, 156, 198, 225, 193, 219, 93, 122, 175, 249, 0,
|
|
22
|
+
175, 143, 70, 239, 46, 246, 163, 53, 163, 109, 168, 135, 2, 235,
|
|
23
|
+
25, 92, 20, 145, 138, 77, 69, 166, 78, 176, 173, 212, 166, 113,
|
|
24
|
+
94, 161, 41, 50, 239, 49, 111, 164, 70, 60, 2, 37, 171, 75,
|
|
25
|
+
136, 156, 11, 56, 42, 146, 138, 229, 73, 146, 77, 61, 98, 196,
|
|
26
|
+
135, 106, 63, 197, 195, 86, 96, 203, 113, 101, 170, 247, 181, 113,
|
|
27
|
+
80, 250, 108, 7, 255, 237, 129, 226, 79, 107, 112, 166, 103, 241,
|
|
28
|
+
24, 223, 239, 120, 198, 58, 60, 82, 128, 3, 184, 66, 143, 224,
|
|
29
|
+
145, 224, 81, 206, 163, 45, 63, 90, 168, 114, 59, 33, 159, 95,
|
|
30
|
+
28, 139, 123, 98, 125, 196, 15, 70, 194, 253, 54, 14, 109, 226,
|
|
31
|
+
71, 17, 161, 93, 186, 87, 244, 138, 20, 52, 123, 251, 26, 36,
|
|
32
|
+
17, 46, 52, 231, 232, 76, 31, 221, 84, 37, 216, 165, 212, 106,
|
|
33
|
+
197, 242, 98, 43, 39, 175, 254, 145, 190, 84, 118, 222, 187, 136,
|
|
34
|
+
120, 163, 236, 249
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
# Index into RNDTABLE. Part of the game state -- include it in desync
|
|
38
|
+
# hashes, and save/restore it alongside player positions.
|
|
39
|
+
attr_accessor :index
|
|
40
|
+
|
|
41
|
+
def initialize(seed = 0)
|
|
42
|
+
reset(seed)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def reset(seed = 0)
|
|
46
|
+
@index = seed & 0xff
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# P_Random(): next table entry, 0-255.
|
|
50
|
+
def byte
|
|
51
|
+
@index = (@index + 1) & 0xff
|
|
52
|
+
RNDTABLE[@index]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Signed difference of two draws, -255..255. DOOM uses this shape a lot
|
|
56
|
+
# (P_Random() - P_Random()) for spread and jitter.
|
|
57
|
+
def diff
|
|
58
|
+
byte - byte
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Kernel#rand-compatible surface so call sites read the same as before.
|
|
62
|
+
#
|
|
63
|
+
# rand -> Float in [0, 1)
|
|
64
|
+
# rand(n) -> Integer in 0...n (DOOM's P_Random()%n)
|
|
65
|
+
# rand(a..b) -> Integer in a..b
|
|
66
|
+
def rand(arg = nil)
|
|
67
|
+
case arg
|
|
68
|
+
when nil
|
|
69
|
+
byte / 256.0
|
|
70
|
+
when ::Range
|
|
71
|
+
lo = arg.begin
|
|
72
|
+
hi = arg.exclude_end? ? arg.end - 1 : arg.end
|
|
73
|
+
span = hi - lo + 1
|
|
74
|
+
span <= 0 ? lo : lo + (byte % span)
|
|
75
|
+
else
|
|
76
|
+
n = arg.to_i
|
|
77
|
+
n <= 0 ? 0 : byte % n
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|