doom 0.4.0 → 0.5.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 +1 -1
- data/lib/doom/game/animations.rb +97 -0
- data/lib/doom/game/combat.rb +244 -0
- data/lib/doom/game/item_pickup.rb +170 -0
- data/lib/doom/game/player_state.rb +101 -9
- data/lib/doom/game/sector_effects.rb +179 -0
- data/lib/doom/platform/gosu_window.rb +536 -66
- data/lib/doom/render/renderer.rb +297 -90
- data/lib/doom/render/status_bar.rb +74 -22
- data/lib/doom/render/weapon_renderer.rb +25 -28
- data/lib/doom/version.rb +1 -1
- data/lib/doom/wad/hud_graphics.rb +70 -2
- data/lib/doom/wad/sprite.rb +95 -22
- data/lib/doom/wad/texture.rb +23 -12
- data/lib/doom.rb +13 -2
- metadata +7 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c4672b03d393f17c1cd9ece97869d7239a7af3e91d3ffa47b911b0ba3058f673
|
|
4
|
+
data.tar.gz: 56772f44ced59386c92f5319c0d25dc8612e96263d68ae7e9a240d26508571cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b11b52ed631217b002aae05a4f87b7ff5fcb208485b75dd9077ebcc222c3823c6ca74167e9bc6b7c3605c0481472abbc818e6445c8fa130c744fe8c54da53099
|
|
7
|
+
data.tar.gz: 808176138162ef4e21796dd995a6bd794eee89e4c631032134349746afd468d446b610a0939daf19c26a2706abac22635306d6e3fb1c1c93e9a70282a2114f7d
|
data/README.md
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Game
|
|
5
|
+
# Animated texture/flat cycling, matching Chocolate Doom's P_InitPicAnims
|
|
6
|
+
# and P_UpdateSpecials from p_spec.c.
|
|
7
|
+
#
|
|
8
|
+
# All animations run at 8 tics per frame (8/35 sec ≈ 0.23s).
|
|
9
|
+
# Frames must be consecutive entries in the WAD; the engine uses
|
|
10
|
+
# start/end names to find the range.
|
|
11
|
+
class Animations
|
|
12
|
+
TICS_PER_FRAME = 8
|
|
13
|
+
|
|
14
|
+
# [is_texture, start_name, end_name]
|
|
15
|
+
# From Chocolate Doom animdefs[] in p_spec.c
|
|
16
|
+
ANIMDEFS = [
|
|
17
|
+
# Animated flats
|
|
18
|
+
[false, 'NUKAGE1', 'NUKAGE3'],
|
|
19
|
+
[false, 'FWATER1', 'FWATER4'],
|
|
20
|
+
[false, 'SWATER1', 'SWATER4'],
|
|
21
|
+
[false, 'LAVA1', 'LAVA4'],
|
|
22
|
+
[false, 'BLOOD1', 'BLOOD3'],
|
|
23
|
+
[false, 'RROCK05', 'RROCK08'],
|
|
24
|
+
[false, 'SLIME01', 'SLIME04'],
|
|
25
|
+
[false, 'SLIME05', 'SLIME08'],
|
|
26
|
+
[false, 'SLIME09', 'SLIME12'],
|
|
27
|
+
# Animated wall textures
|
|
28
|
+
[true, 'BLODGR1', 'BLODGR4'],
|
|
29
|
+
[true, 'SLADRIP1', 'SLADRIP3'],
|
|
30
|
+
[true, 'BLODRIP1', 'BLODRIP4'],
|
|
31
|
+
[true, 'FIREWALA', 'FIREWALL'],
|
|
32
|
+
[true, 'GSTFONT1', 'GSTFONT3'],
|
|
33
|
+
[true, 'FIRELAV3', 'FIRELAVA'],
|
|
34
|
+
[true, 'FIREMAG1', 'FIREMAG3'],
|
|
35
|
+
[true, 'FIREBLU1', 'FIREBLU2'],
|
|
36
|
+
[true, 'ROCKRED1', 'ROCKRED3'],
|
|
37
|
+
[true, 'BFALL1', 'BFALL4'],
|
|
38
|
+
[true, 'SFALL1', 'SFALL4'],
|
|
39
|
+
[true, 'WFALL1', 'WFALL4'],
|
|
40
|
+
[true, 'DBRAIN1', 'DBRAIN4'],
|
|
41
|
+
].freeze
|
|
42
|
+
|
|
43
|
+
attr_reader :flat_translation, :texture_translation
|
|
44
|
+
|
|
45
|
+
def initialize(texture_names, flat_names)
|
|
46
|
+
@flat_translation = {} # flat_name -> current_frame_name
|
|
47
|
+
@texture_translation = {} # texture_name -> current_frame_name
|
|
48
|
+
@anims = []
|
|
49
|
+
|
|
50
|
+
ANIMDEFS.each do |is_texture, start_name, end_name|
|
|
51
|
+
names = is_texture ? texture_names : flat_names
|
|
52
|
+
|
|
53
|
+
start_idx = names.index(start_name)
|
|
54
|
+
end_idx = names.index(end_name)
|
|
55
|
+
next unless start_idx && end_idx
|
|
56
|
+
next if end_idx <= start_idx
|
|
57
|
+
|
|
58
|
+
frames = names[start_idx..end_idx]
|
|
59
|
+
next if frames.size < 2
|
|
60
|
+
|
|
61
|
+
@anims << {
|
|
62
|
+
is_texture: is_texture,
|
|
63
|
+
frames: frames,
|
|
64
|
+
speed: TICS_PER_FRAME,
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Call every game tic (or approximate with leveltime).
|
|
70
|
+
# Matches Chocolate Doom P_UpdateSpecials:
|
|
71
|
+
# pic = basepic + ((leveltime / speed + i) % numpics)
|
|
72
|
+
def update(leveltime)
|
|
73
|
+
@anims.each do |anim|
|
|
74
|
+
frames = anim[:frames]
|
|
75
|
+
numpics = frames.size
|
|
76
|
+
phase = leveltime / anim[:speed]
|
|
77
|
+
translation = anim[:is_texture] ? @texture_translation : @flat_translation
|
|
78
|
+
|
|
79
|
+
numpics.times do |i|
|
|
80
|
+
current_frame = frames[(phase + i) % numpics]
|
|
81
|
+
translation[frames[i]] = current_frame
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Translate a flat name to its current animation frame
|
|
87
|
+
def translate_flat(name)
|
|
88
|
+
@flat_translation[name] || name
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Translate a texture name to its current animation frame
|
|
92
|
+
def translate_texture(name)
|
|
93
|
+
@texture_translation[name] || name
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Game
|
|
5
|
+
# Hitscan weapon firing and monster state tracking.
|
|
6
|
+
# Matches Chocolate Doom's P_LineAttack / P_AimLineAttack from p_map.c.
|
|
7
|
+
class Combat
|
|
8
|
+
# Monster starting HP (from mobjinfo[] in info.c)
|
|
9
|
+
MONSTER_HP = {
|
|
10
|
+
3004 => 20, # Zombieman
|
|
11
|
+
9 => 30, # Shotgun Guy
|
|
12
|
+
3001 => 60, # Imp
|
|
13
|
+
3002 => 150, # Demon
|
|
14
|
+
58 => 150, # Spectre
|
|
15
|
+
3003 => 1000, # Baron of Hell
|
|
16
|
+
69 => 500, # Hell Knight
|
|
17
|
+
3005 => 400, # Cacodemon
|
|
18
|
+
3006 => 100, # Lost Soul
|
|
19
|
+
16 => 4000, # Cyberdemon
|
|
20
|
+
7 => 3000, # Spider Mastermind
|
|
21
|
+
65 => 70, # Heavy Weapon Dude
|
|
22
|
+
64 => 700, # Archvile
|
|
23
|
+
71 => 400, # Pain Elemental
|
|
24
|
+
84 => 20, # Wolfenstein SS
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
MONSTER_RADIUS = {
|
|
28
|
+
3004 => 20, 9 => 20, 3001 => 20, 3002 => 30, 58 => 30,
|
|
29
|
+
3003 => 24, 69 => 24, 3005 => 31, 3006 => 16, 16 => 40,
|
|
30
|
+
7 => 128, 65 => 20, 64 => 20, 71 => 31, 84 => 20,
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
# Normal death frame sequences per sprite prefix (rotation 0 only)
|
|
34
|
+
# Identified by sprite heights: frames go from standing height to flat on ground
|
|
35
|
+
DEATH_FRAMES = {
|
|
36
|
+
'POSS' => %w[H I J K L], # Zombieman: 55→46→34→27→17
|
|
37
|
+
'SPOS' => %w[H I J K L], # Shotgun Guy: 60→50→35→27→17
|
|
38
|
+
'TROO' => %w[I J K L M], # Imp: 62→59→54→46→22
|
|
39
|
+
'SARG' => %w[I J K L M N], # Demon/Spectre: 56→56→53→57→46→32
|
|
40
|
+
'BOSS' => %w[H I J K L M N], # Baron
|
|
41
|
+
'BOS2' => %w[H I J K L M N], # Hell Knight
|
|
42
|
+
'HEAD' => %w[G H I J K L], # Cacodemon
|
|
43
|
+
'SKUL' => %w[G H I J K], # Lost Soul
|
|
44
|
+
'CYBR' => %w[I J], # Cyberdemon
|
|
45
|
+
'SPID' => %w[I J K], # Spider Mastermind
|
|
46
|
+
'CPOS' => %w[H I J K L M N], # Heavy Weapon Dude
|
|
47
|
+
'PAIN' => %w[H I J K L M], # Pain Elemental
|
|
48
|
+
'SSWV' => %w[I J K L M], # Wolfenstein SS
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
51
|
+
DEATH_ANIM_TICS = 6 # Tics per death frame
|
|
52
|
+
|
|
53
|
+
# Weapon damage: DOOM does (P_Random()%3 + 1) * multiplier
|
|
54
|
+
# Pistol/chaingun: 1*5..3*5 = 5-15 per bullet
|
|
55
|
+
# Shotgun: 7 pellets, each 1*5..3*5 = 5-15
|
|
56
|
+
# Fist/chainsaw: 1*2..3*2 = 2-10
|
|
57
|
+
|
|
58
|
+
def initialize(map, player_state, sprites)
|
|
59
|
+
@map = map
|
|
60
|
+
@player = player_state
|
|
61
|
+
@sprites = sprites
|
|
62
|
+
@monster_hp = {} # thing_idx => current HP
|
|
63
|
+
@dead_things = {} # thing_idx => { tic: death_start_tic, prefix: sprite_prefix }
|
|
64
|
+
@tic = 0
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
attr_reader :dead_things
|
|
68
|
+
|
|
69
|
+
def dead?(thing_idx)
|
|
70
|
+
@dead_things.key?(thing_idx)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Get the current death frame sprite for a dead monster
|
|
74
|
+
def death_sprite(thing_idx, thing_type, viewer_angle, thing_angle)
|
|
75
|
+
info = @dead_things[thing_idx]
|
|
76
|
+
return nil unless info
|
|
77
|
+
|
|
78
|
+
frames = DEATH_FRAMES[info[:prefix]]
|
|
79
|
+
return nil unless frames
|
|
80
|
+
|
|
81
|
+
elapsed = @tic - info[:tic]
|
|
82
|
+
frame_idx = (elapsed / DEATH_ANIM_TICS).clamp(0, frames.size - 1)
|
|
83
|
+
frame_letter = frames[frame_idx]
|
|
84
|
+
|
|
85
|
+
@sprites.get_frame(thing_type, frame_letter, viewer_angle, thing_angle)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Called each game tic
|
|
89
|
+
def update
|
|
90
|
+
@tic += 1
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Fire the current weapon
|
|
94
|
+
def fire(px, py, pz, cos_a, sin_a, weapon)
|
|
95
|
+
case weapon
|
|
96
|
+
when PlayerState::WEAPON_PISTOL, PlayerState::WEAPON_CHAINGUN
|
|
97
|
+
hitscan(px, py, cos_a, sin_a, 1, 0.0, 5)
|
|
98
|
+
when PlayerState::WEAPON_SHOTGUN
|
|
99
|
+
hitscan(px, py, cos_a, sin_a, 7, Math::PI / 32, 5)
|
|
100
|
+
when PlayerState::WEAPON_FIST
|
|
101
|
+
melee(px, py, cos_a, sin_a, 2, 64)
|
|
102
|
+
when PlayerState::WEAPON_CHAINSAW
|
|
103
|
+
melee(px, py, cos_a, sin_a, 2, 64)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def hitscan(px, py, cos_a, sin_a, pellets, spread, multiplier)
|
|
110
|
+
pellets.times do
|
|
111
|
+
# Add random spread
|
|
112
|
+
if spread > 0
|
|
113
|
+
angle = Math.atan2(sin_a, cos_a) + (rand - 0.5) * spread * 2
|
|
114
|
+
ca = Math.cos(angle)
|
|
115
|
+
sa = Math.sin(angle)
|
|
116
|
+
else
|
|
117
|
+
# Slight pistol/chaingun spread
|
|
118
|
+
angle = Math.atan2(sin_a, cos_a) + (rand - 0.5) * 0.04
|
|
119
|
+
ca = Math.cos(angle)
|
|
120
|
+
sa = Math.sin(angle)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
wall_dist = trace_wall(px, py, ca, sa)
|
|
124
|
+
|
|
125
|
+
best_idx = nil
|
|
126
|
+
best_dist = wall_dist
|
|
127
|
+
|
|
128
|
+
@map.things.each_with_index do |thing, idx|
|
|
129
|
+
next unless MONSTER_HP[thing.type]
|
|
130
|
+
next if @dead_things[idx]
|
|
131
|
+
|
|
132
|
+
radius = MONSTER_RADIUS[thing.type] || 20
|
|
133
|
+
hit_dist = ray_circle_hit(px, py, ca, sa, thing.x, thing.y, radius)
|
|
134
|
+
if hit_dist && hit_dist > 0 && hit_dist < best_dist
|
|
135
|
+
best_dist = hit_dist
|
|
136
|
+
best_idx = idx
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
if best_idx
|
|
141
|
+
damage = (rand(3) + 1) * multiplier
|
|
142
|
+
apply_damage(best_idx, damage)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def melee(px, py, cos_a, sin_a, multiplier, range)
|
|
148
|
+
best_idx = nil
|
|
149
|
+
best_dist = range.to_f
|
|
150
|
+
|
|
151
|
+
@map.things.each_with_index do |thing, idx|
|
|
152
|
+
next unless MONSTER_HP[thing.type]
|
|
153
|
+
next if @dead_things[idx]
|
|
154
|
+
|
|
155
|
+
dx = thing.x - px
|
|
156
|
+
dy = thing.y - py
|
|
157
|
+
dist = Math.sqrt(dx * dx + dy * dy)
|
|
158
|
+
next if dist > range + (MONSTER_RADIUS[thing.type] || 20)
|
|
159
|
+
|
|
160
|
+
# Check if roughly facing the monster
|
|
161
|
+
dot = dx * cos_a + dy * sin_a
|
|
162
|
+
next if dot < 0
|
|
163
|
+
|
|
164
|
+
if dist < best_dist
|
|
165
|
+
best_dist = dist
|
|
166
|
+
best_idx = idx
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
if best_idx
|
|
171
|
+
damage = (rand(3) + 1) * multiplier
|
|
172
|
+
apply_damage(best_idx, damage)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def apply_damage(thing_idx, damage)
|
|
177
|
+
thing = @map.things[thing_idx]
|
|
178
|
+
@monster_hp[thing_idx] ||= MONSTER_HP[thing.type] || 20
|
|
179
|
+
|
|
180
|
+
@monster_hp[thing_idx] -= damage
|
|
181
|
+
|
|
182
|
+
if @monster_hp[thing_idx] <= 0
|
|
183
|
+
prefix = @sprites.prefix_for(thing.type)
|
|
184
|
+
@dead_things[thing_idx] = { tic: @tic, prefix: prefix } if prefix
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def trace_wall(px, py, cos_a, sin_a)
|
|
189
|
+
best_t = 4096.0 # Max hitscan range
|
|
190
|
+
|
|
191
|
+
@map.linedefs.each do |ld|
|
|
192
|
+
v1 = @map.vertices[ld.v1]
|
|
193
|
+
v2 = @map.vertices[ld.v2]
|
|
194
|
+
|
|
195
|
+
# One-sided always blocks; two-sided only if impassable
|
|
196
|
+
blocks = (ld.sidedef_left == 0xFFFF) || (ld.flags & 0x0001 != 0)
|
|
197
|
+
unless blocks
|
|
198
|
+
next unless ld.sidedef_left < 0xFFFF
|
|
199
|
+
front = @map.sidedefs[ld.sidedef_right]
|
|
200
|
+
back = @map.sidedefs[ld.sidedef_left]
|
|
201
|
+
fs = @map.sectors[front.sector]
|
|
202
|
+
bs = @map.sectors[back.sector]
|
|
203
|
+
# Blocks if opening is too small (step or low ceiling)
|
|
204
|
+
max_floor = [fs.floor_height, bs.floor_height].max
|
|
205
|
+
min_ceil = [fs.ceiling_height, bs.ceiling_height].min
|
|
206
|
+
blocks = (min_ceil - max_floor) < 56
|
|
207
|
+
end
|
|
208
|
+
next unless blocks
|
|
209
|
+
|
|
210
|
+
t = ray_segment_intersect(px, py, cos_a, sin_a,
|
|
211
|
+
v1.x, v1.y, v2.x, v2.y)
|
|
212
|
+
best_t = t if t && t > 0 && t < best_t
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
best_t
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def ray_segment_intersect(px, py, dx, dy, x1, y1, x2, y2)
|
|
219
|
+
sx = x2 - x1
|
|
220
|
+
sy = y2 - y1
|
|
221
|
+
denom = dx * sy - dy * sx
|
|
222
|
+
return nil if denom.abs < 0.001
|
|
223
|
+
|
|
224
|
+
t = ((x1 - px) * sy - (y1 - py) * sx) / denom
|
|
225
|
+
u = ((x1 - px) * dy - (y1 - py) * dx) / denom
|
|
226
|
+
|
|
227
|
+
(t > 0 && u >= 0.0 && u <= 1.0) ? t : nil
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def ray_circle_hit(px, py, cos_a, sin_a, cx, cy, radius)
|
|
231
|
+
dx = cx - px
|
|
232
|
+
dy = cy - py
|
|
233
|
+
proj = dx * cos_a + dy * sin_a
|
|
234
|
+
return nil if proj < 0
|
|
235
|
+
|
|
236
|
+
perp_sq = dx * dx + dy * dy - proj * proj
|
|
237
|
+
return nil if perp_sq > radius * radius
|
|
238
|
+
|
|
239
|
+
chord_half = Math.sqrt([radius * radius - perp_sq, 0].max)
|
|
240
|
+
proj - chord_half
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doom
|
|
4
|
+
module Game
|
|
5
|
+
# Handles item pickup when player touches things.
|
|
6
|
+
# Matches Chocolate Doom's P_TouchSpecialThing from p_inter.c.
|
|
7
|
+
class ItemPickup
|
|
8
|
+
PLAYER_RADIUS = 16.0
|
|
9
|
+
THING_RADIUS = 20.0
|
|
10
|
+
PICKUP_DIST = PLAYER_RADIUS + THING_RADIUS # 36 units (bounding box overlap)
|
|
11
|
+
|
|
12
|
+
# Item definitions: type => { category, value, ... }
|
|
13
|
+
ITEMS = {
|
|
14
|
+
# Weapons (give weapon + some ammo)
|
|
15
|
+
2001 => { cat: :weapon, weapon: 2, ammo: :shells, amount: 8 }, # Shotgun
|
|
16
|
+
2002 => { cat: :weapon, weapon: 3, ammo: :bullets, amount: 20 }, # Chaingun
|
|
17
|
+
2003 => { cat: :weapon, weapon: 4, ammo: :rockets, amount: 2 }, # Rocket launcher
|
|
18
|
+
2004 => { cat: :weapon, weapon: 5, ammo: :cells, amount: 40 }, # Plasma rifle
|
|
19
|
+
2006 => { cat: :weapon, weapon: 6, ammo: :cells, amount: 40 }, # BFG9000
|
|
20
|
+
2005 => { cat: :weapon, weapon: 7, ammo: :bullets, amount: 0 }, # Chainsaw
|
|
21
|
+
|
|
22
|
+
# Ammo
|
|
23
|
+
2007 => { cat: :ammo, ammo: :bullets, amount: 10 }, # Clip
|
|
24
|
+
2048 => { cat: :ammo, ammo: :bullets, amount: 50 }, # Box of bullets
|
|
25
|
+
2008 => { cat: :ammo, ammo: :shells, amount: 4 }, # 4 shells
|
|
26
|
+
2049 => { cat: :ammo, ammo: :shells, amount: 20 }, # Box of shells
|
|
27
|
+
2010 => { cat: :ammo, ammo: :rockets, amount: 1 }, # Rocket
|
|
28
|
+
2046 => { cat: :ammo, ammo: :rockets, amount: 5 }, # Box of rockets
|
|
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
|
+
|
|
33
|
+
# Health
|
|
34
|
+
2014 => { cat: :health, amount: 1, max: 200 }, # Health bonus (+1, up to 200)
|
|
35
|
+
2011 => { cat: :health, amount: 10, max: 100 }, # Stimpack
|
|
36
|
+
2012 => { cat: :health, amount: 25, max: 100 }, # Medikit
|
|
37
|
+
2013 => { cat: :health, amount: 100, max: 200 }, # Soul sphere
|
|
38
|
+
|
|
39
|
+
# Armor
|
|
40
|
+
2015 => { cat: :armor, amount: 1, max: 200 }, # Armor bonus (+1, up to 200)
|
|
41
|
+
2018 => { cat: :armor, amount: 100, armor_type: 1 }, # Green armor (100%, absorbs 1/3)
|
|
42
|
+
2019 => { cat: :armor, amount: 200, armor_type: 2 }, # Blue armor (200%, absorbs 1/2)
|
|
43
|
+
|
|
44
|
+
# Keys
|
|
45
|
+
5 => { cat: :key, key: :blue_card },
|
|
46
|
+
6 => { cat: :key, key: :yellow_card },
|
|
47
|
+
13 => { cat: :key, key: :red_card },
|
|
48
|
+
40 => { cat: :key, key: :blue_skull },
|
|
49
|
+
39 => { cat: :key, key: :yellow_skull },
|
|
50
|
+
38 => { cat: :key, key: :red_skull },
|
|
51
|
+
}.freeze
|
|
52
|
+
|
|
53
|
+
attr_reader :picked_up
|
|
54
|
+
|
|
55
|
+
def initialize(map, player_state)
|
|
56
|
+
@map = map
|
|
57
|
+
@player = player_state
|
|
58
|
+
@picked_up = {} # thing index => true (to avoid re-picking)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def update(player_x, player_y)
|
|
62
|
+
@map.things.each_with_index do |thing, idx|
|
|
63
|
+
next if @picked_up[idx]
|
|
64
|
+
item = ITEMS[thing.type]
|
|
65
|
+
next unless item
|
|
66
|
+
|
|
67
|
+
# DOOM uses bounding box overlap: abs(dx) < sum_of_radii
|
|
68
|
+
dx = (player_x - thing.x).abs
|
|
69
|
+
dy = (player_y - thing.y).abs
|
|
70
|
+
next if dx >= PICKUP_DIST || dy >= PICKUP_DIST
|
|
71
|
+
|
|
72
|
+
if try_pickup(item)
|
|
73
|
+
@picked_up[idx] = true
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def try_pickup(item)
|
|
81
|
+
case item[:cat]
|
|
82
|
+
when :weapon
|
|
83
|
+
give_weapon(item)
|
|
84
|
+
when :ammo
|
|
85
|
+
give_ammo(item[:ammo], item[:amount])
|
|
86
|
+
when :backpack
|
|
87
|
+
give_backpack
|
|
88
|
+
when :health
|
|
89
|
+
give_health(item[:amount], item[:max])
|
|
90
|
+
when :armor
|
|
91
|
+
give_armor(item)
|
|
92
|
+
when :key
|
|
93
|
+
give_key(item[:key])
|
|
94
|
+
else
|
|
95
|
+
false
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def give_weapon(item)
|
|
100
|
+
weapon_idx = item[:weapon]
|
|
101
|
+
had_weapon = @player.has_weapons[weapon_idx]
|
|
102
|
+
|
|
103
|
+
@player.has_weapons[weapon_idx] = true
|
|
104
|
+
ammo_given = item[:ammo] ? give_ammo(item[:ammo], item[:amount]) : false
|
|
105
|
+
|
|
106
|
+
unless had_weapon
|
|
107
|
+
@player.switch_weapon(weapon_idx) unless @player.attacking
|
|
108
|
+
return true
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
ammo_given
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def give_ammo(type, amount)
|
|
115
|
+
case type
|
|
116
|
+
when :bullets
|
|
117
|
+
return false if @player.ammo_bullets >= @player.max_bullets
|
|
118
|
+
@player.ammo_bullets = [@player.ammo_bullets + amount, @player.max_bullets].min
|
|
119
|
+
when :shells
|
|
120
|
+
return false if @player.ammo_shells >= @player.max_shells
|
|
121
|
+
@player.ammo_shells = [@player.ammo_shells + amount, @player.max_shells].min
|
|
122
|
+
when :rockets
|
|
123
|
+
return false if @player.ammo_rockets >= @player.max_rockets
|
|
124
|
+
@player.ammo_rockets = [@player.ammo_rockets + amount, @player.max_rockets].min
|
|
125
|
+
when :cells
|
|
126
|
+
return false if @player.ammo_cells >= @player.max_cells
|
|
127
|
+
@player.ammo_cells = [@player.ammo_cells + amount, @player.max_cells].min
|
|
128
|
+
end
|
|
129
|
+
true
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def give_backpack
|
|
133
|
+
@player.max_bullets = 400
|
|
134
|
+
@player.max_shells = 100
|
|
135
|
+
@player.max_rockets = 100
|
|
136
|
+
@player.max_cells = 600
|
|
137
|
+
give_ammo(:bullets, 10)
|
|
138
|
+
give_ammo(:shells, 4)
|
|
139
|
+
give_ammo(:rockets, 1)
|
|
140
|
+
give_ammo(:cells, 20)
|
|
141
|
+
true
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def give_health(amount, max)
|
|
145
|
+
return false if @player.health >= max
|
|
146
|
+
@player.health = [@player.health + amount, max].min
|
|
147
|
+
true
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def give_armor(item)
|
|
151
|
+
if item[:armor_type]
|
|
152
|
+
# Green/blue armor: only pick up if better than current
|
|
153
|
+
return false if @player.armor >= item[:amount]
|
|
154
|
+
@player.armor = item[:amount]
|
|
155
|
+
else
|
|
156
|
+
# Armor bonus: +1, up to max
|
|
157
|
+
return false if @player.armor >= item[:max]
|
|
158
|
+
@player.armor = [@player.armor + item[:amount], item[:max]].min
|
|
159
|
+
end
|
|
160
|
+
true
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def give_key(key)
|
|
164
|
+
return false if @player.keys[key]
|
|
165
|
+
@player.keys[key] = true
|
|
166
|
+
true
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -26,16 +26,16 @@ module Doom
|
|
|
26
26
|
WEAPON_CHAINSAW => :chainsaw
|
|
27
27
|
}.freeze
|
|
28
28
|
|
|
29
|
-
# Attack durations
|
|
29
|
+
# Attack durations in tics (at 35fps), matching DOOM's weapon state sequences
|
|
30
30
|
ATTACK_DURATIONS = {
|
|
31
|
-
WEAPON_FIST =>
|
|
32
|
-
WEAPON_PISTOL =>
|
|
33
|
-
WEAPON_SHOTGUN =>
|
|
34
|
-
WEAPON_CHAINGUN =>
|
|
35
|
-
WEAPON_ROCKET =>
|
|
36
|
-
WEAPON_PLASMA =>
|
|
37
|
-
WEAPON_BFG =>
|
|
38
|
-
WEAPON_CHAINSAW =>
|
|
31
|
+
WEAPON_FIST => 14, # Punch windup + swing
|
|
32
|
+
WEAPON_PISTOL => 16, # S_PISTOL: 6+4+5+1 tics
|
|
33
|
+
WEAPON_SHOTGUN => 40, # Pump action cycle
|
|
34
|
+
WEAPON_CHAINGUN => 8, # Rapid fire (2 shots per cycle)
|
|
35
|
+
WEAPON_ROCKET => 20, # Rocket launch + recovery
|
|
36
|
+
WEAPON_PLASMA => 8, # Fast energy weapon
|
|
37
|
+
WEAPON_BFG => 60, # Long charge + fire
|
|
38
|
+
WEAPON_CHAINSAW => 6 # Fast melee
|
|
39
39
|
}.freeze
|
|
40
40
|
|
|
41
41
|
attr_accessor :health, :armor, :max_health, :max_armor
|
|
@@ -47,6 +47,26 @@ module Doom
|
|
|
47
47
|
attr_accessor :bob_angle, :bob_amount
|
|
48
48
|
attr_accessor :is_moving
|
|
49
49
|
|
|
50
|
+
# Smooth step-up/down (matching Chocolate Doom's P_CalcHeight / P_ZMovement)
|
|
51
|
+
VIEWHEIGHT = 41.0
|
|
52
|
+
VIEWHEIGHT_HALF = VIEWHEIGHT / 2.0
|
|
53
|
+
DELTA_ACCEL = 0.25 # deltaviewheight += FRACUNIT/4 per tic
|
|
54
|
+
attr_reader :viewheight, :deltaviewheight
|
|
55
|
+
|
|
56
|
+
# View bob (camera bounce when walking, matching Chocolate Doom's
|
|
57
|
+
# P_CalcHeight + P_XYMovement + P_Thrust from p_user.c / p_mobj.c)
|
|
58
|
+
MAXBOB = 16.0 # Maximum bob amplitude (0x100000 in fixed-point = 16 map units)
|
|
59
|
+
STOPSPEED = 0.0625 # Snap-to-zero threshold (0x1000 in fixed-point)
|
|
60
|
+
# Continuous-time equivalents of DOOM's per-tic constants (35 fps tic rate):
|
|
61
|
+
# FRICTION = 0xE800/0x10000 = 0.90625 per tic
|
|
62
|
+
# decay_rate = -ln(0.90625) * 35 = 3.44/sec
|
|
63
|
+
# walk thrust = forwardmove(25) * 2048 / 65536 = 0.78 map units/tic = 27.3/sec
|
|
64
|
+
# terminal velocity = 27.3 / 3.44 = 7.56 -> bob = 7.56^2/4 = 14.3 (89% of MAXBOB)
|
|
65
|
+
BOB_DECAY_RATE = 3.44 # Friction as continuous decay rate (1/sec)
|
|
66
|
+
BOB_THRUST = 26.0 # Walk thrust (map units/sec), gives terminal ~7.5
|
|
67
|
+
BOB_FREQUENCY = 11.0 # Bob cycle frequency (rad/sec): FINEANGLES/20 * 35 / 8192 * 2*PI
|
|
68
|
+
attr_reader :view_bob_offset
|
|
69
|
+
|
|
50
70
|
def initialize
|
|
51
71
|
reset
|
|
52
72
|
end
|
|
@@ -91,6 +111,18 @@ module Doom
|
|
|
91
111
|
@bob_angle = 0.0
|
|
92
112
|
@bob_amount = 0.0
|
|
93
113
|
@is_moving = false
|
|
114
|
+
|
|
115
|
+
# Smooth step height (P_CalcHeight viewheight/deltaviewheight)
|
|
116
|
+
@viewheight = VIEWHEIGHT
|
|
117
|
+
@deltaviewheight = 0.0
|
|
118
|
+
|
|
119
|
+
# View bob (camera bounce) - simulated momentum for P_CalcHeight
|
|
120
|
+
@view_bob_offset = 0.0
|
|
121
|
+
@momx = 0.0 # Simulated X momentum (map units/sec, not actual movement)
|
|
122
|
+
@momy = 0.0 # Simulated Y momentum
|
|
123
|
+
@thrust_x = 0.0 # Per-frame thrust input (raw, before normalization)
|
|
124
|
+
@thrust_y = 0.0
|
|
125
|
+
@view_bob_angle = 0.0
|
|
94
126
|
end
|
|
95
127
|
|
|
96
128
|
def weapon_name
|
|
@@ -190,6 +222,66 @@ module Doom
|
|
|
190
222
|
end
|
|
191
223
|
end
|
|
192
224
|
|
|
225
|
+
# Called when player moves onto a different floor height.
|
|
226
|
+
# Matches Chocolate Doom P_ZMovement: reduce viewheight by the step amount
|
|
227
|
+
# so the camera doesn't snap, then let P_CalcHeight recover it smoothly.
|
|
228
|
+
def notify_step(step_amount)
|
|
229
|
+
return if step_amount == 0
|
|
230
|
+
@viewheight -= step_amount
|
|
231
|
+
@deltaviewheight = (VIEWHEIGHT - @viewheight) / 8.0
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Gradually restore viewheight to VIEWHEIGHT (called each tic).
|
|
235
|
+
# Matches Chocolate Doom P_CalcHeight viewheight recovery loop.
|
|
236
|
+
# For step-up: viewheight < 41, delta > 0, accelerates upward.
|
|
237
|
+
# For step-down: viewheight > 41, delta < 0, decelerates then recovers.
|
|
238
|
+
def update_viewheight
|
|
239
|
+
@viewheight += @deltaviewheight
|
|
240
|
+
|
|
241
|
+
if @viewheight > VIEWHEIGHT && @deltaviewheight >= 0
|
|
242
|
+
@viewheight = VIEWHEIGHT
|
|
243
|
+
@deltaviewheight = 0.0
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
if @viewheight < VIEWHEIGHT_HALF
|
|
247
|
+
@viewheight = VIEWHEIGHT_HALF
|
|
248
|
+
@deltaviewheight = 1.0 if @deltaviewheight <= 0
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
if @deltaviewheight != 0
|
|
252
|
+
@deltaviewheight += DELTA_ACCEL
|
|
253
|
+
@deltaviewheight = 0.0 if @deltaviewheight.abs < 0.01 && (@viewheight - VIEWHEIGHT).abs < 0.5
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Set movement momentum directly (called from GosuWindow with actual
|
|
258
|
+
# movement momentum, which already has thrust + friction applied).
|
|
259
|
+
def set_movement_momentum(momx, momy)
|
|
260
|
+
@momx = momx
|
|
261
|
+
@momy = momy
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Compute view bob from actual movement momentum.
|
|
265
|
+
# Matches Chocolate Doom P_CalcHeight:
|
|
266
|
+
# bob = (momx*momx + momy*momy) >> 2, capped at MAXBOB
|
|
267
|
+
# viewz += finesine[angle] * bob/2
|
|
268
|
+
# Momentum is in units/sec; DOOM's bob uses units/tic (divide by 35).
|
|
269
|
+
BOB_MOM_SCALE = 1.0 / (35.0 * 35.0 * 4.0) # (mom/35)^2 / 4
|
|
270
|
+
|
|
271
|
+
def update_view_bob(delta_time)
|
|
272
|
+
dt = delta_time.clamp(0.001, 0.05)
|
|
273
|
+
|
|
274
|
+
# P_CalcHeight: bob = (momx_per_tic^2 + momy_per_tic^2) / 4, capped at MAXBOB
|
|
275
|
+
bob = (@momx * @momx + @momy * @momy) * BOB_MOM_SCALE
|
|
276
|
+
bob = MAXBOB if bob > MAXBOB
|
|
277
|
+
|
|
278
|
+
# Advance bob sine wave (FINEANGLES/20 per tic = ~11 rad/sec)
|
|
279
|
+
@view_bob_angle += BOB_FREQUENCY * dt
|
|
280
|
+
|
|
281
|
+
# viewz offset: sin(angle) * bob/2
|
|
282
|
+
@view_bob_offset = Math.sin(@view_bob_angle) * bob / 2.0
|
|
283
|
+
end
|
|
284
|
+
|
|
193
285
|
def weapon_bob_x
|
|
194
286
|
Math.cos(@bob_angle) * @bob_amount
|
|
195
287
|
end
|