quake-rb 0.1.0 → 0.2.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 +136 -0
- data/bin/quake +18 -1
- data/lib/quake/bsp/reader.rb +241 -38
- data/lib/quake/bsp/types.rb +49 -5
- data/lib/quake/bsp/vis.rb +2 -137
- data/lib/quake/camera.rb +73 -16
- data/lib/quake/entity.rb +413 -25
- data/lib/quake/game/brush_entities.rb +1814 -65
- data/lib/quake/game/engine.rb +4376 -57
- data/lib/quake/game/item_pickups.rb +584 -33
- data/lib/quake/game/player_state.rb +518 -21
- data/lib/quake/mdl/reader.rb +88 -7
- data/lib/quake/mdl/types.rb +2 -2
- data/lib/quake/pak/reader.rb +9 -3
- data/lib/quake/palette.rb +3 -4
- data/lib/quake/physics/hull_trace.rb +77 -4
- data/lib/quake/physics/player.rb +409 -112
- data/lib/quake/renderer/anorm_dots.rb +554 -0
- data/lib/quake/renderer/gl_alias_model.rb +418 -69
- data/lib/quake/renderer/gl_brush_model.rb +129 -17
- data/lib/quake/renderer/gl_hud.rb +384 -31
- data/lib/quake/renderer/gl_lightmap.rb +224 -48
- data/lib/quake/renderer/gl_particles.rb +390 -50
- data/lib/quake/renderer/gl_sky.rb +83 -10
- data/lib/quake/renderer/gl_texture_manager.rb +38 -4
- data/lib/quake/renderer/gl_textured.rb +53 -31
- data/lib/quake/renderer/gl_view_blend.rb +130 -0
- data/lib/quake/renderer/gl_viewmodel.rb +46 -11
- data/lib/quake/renderer/gl_warp_subdivision.rb +74 -0
- data/lib/quake/renderer/gl_water.rb +4 -76
- data/lib/quake/sound/events.rb +126 -2
- data/lib/quake/sound/mixer.rb +44 -9
- data/lib/quake/version.rb +1 -1
- data/lib/quake/wad/reader.rb +18 -8
- data/lib/quake/window.rb +3 -0
- metadata +5 -1
|
@@ -15,10 +15,30 @@ module Quake
|
|
|
15
15
|
VIRTUAL_HEIGHT = 200
|
|
16
16
|
SBAR_HEIGHT = 24 # status bar height
|
|
17
17
|
DIGIT_WIDTH = 24 # big number digit width
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
CHAR_SIZE = 8
|
|
19
|
+
WEAPON_ICONS = [
|
|
20
|
+
[:shotgun, "shotgun"],
|
|
21
|
+
[:super_shotgun, "sshotgun"],
|
|
22
|
+
[:nailgun, "nailgun"],
|
|
23
|
+
[:super_nailgun, "snailgun"],
|
|
24
|
+
[:grenade_launcher, "rlaunch"],
|
|
25
|
+
[:rocket_launcher, "srlaunch"],
|
|
26
|
+
[:lightning_gun, "lightng"]
|
|
27
|
+
].freeze
|
|
28
|
+
INVENTORY_ITEMS = [
|
|
29
|
+
[:key, :silver, "sb_key1"],
|
|
30
|
+
[:key, :gold, "sb_key2"],
|
|
31
|
+
[:powerup, :ring, "sb_invis"],
|
|
32
|
+
[:powerup, :pentagram, "sb_invuln"],
|
|
33
|
+
[:powerup, :biosuit, "sb_suit"],
|
|
34
|
+
[:powerup, :quad, "sb_quad"]
|
|
35
|
+
].freeze
|
|
36
|
+
INVENTORY_AMMO_TYPES = %i[shells nails rockets cells].freeze
|
|
37
|
+
|
|
38
|
+
def initialize(wad, palette, screen_width, screen_height, pak: nil)
|
|
20
39
|
@wad = wad
|
|
21
40
|
@palette = palette
|
|
41
|
+
@pak = pak
|
|
22
42
|
@screen_width = screen_width
|
|
23
43
|
@screen_height = screen_height
|
|
24
44
|
@textures = {} # name -> { id:, width:, height: }
|
|
@@ -26,7 +46,7 @@ module Quake
|
|
|
26
46
|
upload_hud_graphics
|
|
27
47
|
end
|
|
28
48
|
|
|
29
|
-
def render(player_state)
|
|
49
|
+
def render(player_state, time: 0.0, stats: nil)
|
|
30
50
|
setup_ortho
|
|
31
51
|
|
|
32
52
|
GL.Enable(GL::TEXTURE_2D)
|
|
@@ -35,7 +55,7 @@ module Quake
|
|
|
35
55
|
GL.Disable(GL::DEPTH_TEST)
|
|
36
56
|
GL.Color4f(1.0, 1.0, 1.0, 1.0)
|
|
37
57
|
|
|
38
|
-
|
|
58
|
+
draw_hud(player_state, time: time, stats: stats)
|
|
39
59
|
|
|
40
60
|
GL.Enable(GL::DEPTH_TEST)
|
|
41
61
|
GL.Disable(GL::BLEND)
|
|
@@ -45,18 +65,58 @@ module Quake
|
|
|
45
65
|
|
|
46
66
|
private
|
|
47
67
|
|
|
68
|
+
# During intermission Quake draws only Sbar_IntermissionOverlay --
|
|
69
|
+
# no status bar and no crosshair (screen.c SCR_UpdateScreen).
|
|
70
|
+
def draw_hud(player_state, time:, stats: nil)
|
|
71
|
+
if stats && stats[:intermission]
|
|
72
|
+
draw_intermission(stats)
|
|
73
|
+
else
|
|
74
|
+
draw_crosshair
|
|
75
|
+
if stats
|
|
76
|
+
draw_notify_lines(stats[:notify_lines])
|
|
77
|
+
draw_centerprint(stats[:centerprint])
|
|
78
|
+
end
|
|
79
|
+
# sb_lines from viewsize: 0 hides the bar, 24 shows the status
|
|
80
|
+
# bar only, more adds the inventory bar (screen.c/sbar.c)
|
|
81
|
+
sb_lines = stats ? stats.fetch(:sb_lines, 40) : 40
|
|
82
|
+
return if sb_lines <= 0
|
|
83
|
+
|
|
84
|
+
draw_sbar(player_state, time: time, stats: stats, inventory: sb_lines > 24)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
48
88
|
def setup_ortho
|
|
49
89
|
GL.MatrixMode(GL::PROJECTION)
|
|
50
90
|
GL.PushMatrix
|
|
51
91
|
GL.LoadIdentity
|
|
52
|
-
# Y=0 at bottom, Y=200 at top (standard GL orientation)
|
|
53
|
-
|
|
92
|
+
# Y=0 at bottom, Y=200 at top (standard GL orientation).
|
|
93
|
+
# Width is aspect-corrected so the HUD doesn't stretch on
|
|
94
|
+
# non-320x200-shaped windows.
|
|
95
|
+
GL.Ortho(0, virtual_width, 0, VIRTUAL_HEIGHT, -1, 1)
|
|
54
96
|
|
|
55
97
|
GL.MatrixMode(GL::MODELVIEW)
|
|
56
98
|
GL.PushMatrix
|
|
57
99
|
GL.LoadIdentity
|
|
58
100
|
end
|
|
59
101
|
|
|
102
|
+
# Virtual screen width matching the window aspect ratio at 200
|
|
103
|
+
# virtual pixels tall. Never narrower than the 320px status bar.
|
|
104
|
+
def virtual_width
|
|
105
|
+
@virtual_width ||=
|
|
106
|
+
if @screen_width && @screen_height.to_f.positive?
|
|
107
|
+
[(VIRTUAL_HEIGHT * @screen_width.to_f / @screen_height).round,
|
|
108
|
+
VIRTUAL_WIDTH].max
|
|
109
|
+
else
|
|
110
|
+
VIRTUAL_WIDTH
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Horizontal offset centering the 320-wide status bar, like Quake's
|
|
115
|
+
# (vid.width - 320) >> 1 in Sbar_DrawPic (sbar.c).
|
|
116
|
+
def sbar_x
|
|
117
|
+
@sbar_x ||= (virtual_width - VIRTUAL_WIDTH) / 2
|
|
118
|
+
end
|
|
119
|
+
|
|
60
120
|
def restore_projection
|
|
61
121
|
GL.MatrixMode(GL::MODELVIEW)
|
|
62
122
|
GL.PopMatrix
|
|
@@ -84,39 +144,243 @@ module Quake
|
|
|
84
144
|
GL.End
|
|
85
145
|
end
|
|
86
146
|
|
|
87
|
-
def draw_sbar(ps)
|
|
147
|
+
def draw_sbar(ps, time: 0.0, stats: nil, inventory: true)
|
|
148
|
+
draw_inventory(ps, time: time) if inventory
|
|
149
|
+
|
|
88
150
|
# Sbar sits at the very bottom of the screen
|
|
89
151
|
sbar_y = 0
|
|
90
152
|
|
|
91
|
-
|
|
92
|
-
|
|
153
|
+
if ps.health <= 0
|
|
154
|
+
draw_pic(sbar_x, sbar_y, "scorebar")
|
|
155
|
+
draw_solo_scoreboard(stats) if stats
|
|
156
|
+
return
|
|
157
|
+
end
|
|
93
158
|
|
|
94
|
-
#
|
|
95
|
-
|
|
159
|
+
# Status bar background (320x24)
|
|
160
|
+
draw_pic(sbar_x, sbar_y, "sbar")
|
|
96
161
|
|
|
97
|
-
|
|
98
|
-
if ps.armor > 0
|
|
99
|
-
armor_icon = case ps.armor_type
|
|
100
|
-
when 1 then "sb_armor1"
|
|
101
|
-
when 2 then "sb_armor2"
|
|
102
|
-
else "sb_armor3"
|
|
103
|
-
end
|
|
104
|
-
draw_pic(0, sbar_y, armor_icon) if @textures[armor_icon]
|
|
105
|
-
end
|
|
162
|
+
draw_armor(ps, sbar_y, time: time)
|
|
106
163
|
|
|
107
164
|
# Face (x=112)
|
|
108
|
-
draw_pic(112, sbar_y, health_face(ps
|
|
165
|
+
draw_pic(sbar_x + 112, sbar_y, health_face(ps, time: time))
|
|
109
166
|
|
|
110
167
|
# Health (x=136, 3 digits)
|
|
111
|
-
draw_num(136, sbar_y, ps.health, 3, ps.health <= 25)
|
|
168
|
+
draw_num(sbar_x + 136, sbar_y, ps.health, 3, ps.health <= 25)
|
|
112
169
|
|
|
113
170
|
# Ammo icon (x=224)
|
|
114
171
|
ammo_icon = ammo_type_icon(ps.current_ammo_type)
|
|
115
|
-
draw_pic(224, sbar_y, ammo_icon) if ammo_icon && @textures[ammo_icon]
|
|
172
|
+
draw_pic(sbar_x + 224, sbar_y, ammo_icon) if ammo_icon && @textures[ammo_icon]
|
|
116
173
|
|
|
117
174
|
# Ammo count (x=248, 3 digits)
|
|
118
175
|
ammo = ps.current_ammo_count
|
|
119
|
-
draw_num(248, sbar_y, ammo, 3, ammo && ammo <= 10) if ammo
|
|
176
|
+
draw_num(sbar_x + 248, sbar_y, ammo, 3, ammo && ammo <= 10) if ammo
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def draw_inventory(ps, time: 0.0)
|
|
180
|
+
draw_pic(sbar_x, SBAR_HEIGHT, "ibar")
|
|
181
|
+
|
|
182
|
+
WEAPON_ICONS.each_with_index do |(weapon, icon), index|
|
|
183
|
+
next unless ps.weapons_owned.include?(weapon)
|
|
184
|
+
|
|
185
|
+
variant = weapon_flash_variant(ps, weapon, index, time)
|
|
186
|
+
draw_pic(sbar_x + (index * 24), SBAR_HEIGHT, "#{variant}_#{icon}")
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
draw_inventory_ammo(ps)
|
|
190
|
+
draw_inventory_items(ps, time: time)
|
|
191
|
+
draw_sigils(ps)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def draw_armor(ps, y, time: 0.0)
|
|
195
|
+
if powerup_item_active?(ps, :pentagram, time)
|
|
196
|
+
draw_num(sbar_x + 24, y, 666, 3, true)
|
|
197
|
+
draw_pic(sbar_x, y, "disc")
|
|
198
|
+
return
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
draw_num(sbar_x + 24, y, ps.armor, 3, ps.armor <= 25)
|
|
202
|
+
return unless ps.armor > 0
|
|
203
|
+
|
|
204
|
+
armor_icon = case ps.armor_type
|
|
205
|
+
when 1 then "sb_armor1"
|
|
206
|
+
when 2 then "sb_armor2"
|
|
207
|
+
else "sb_armor3"
|
|
208
|
+
end
|
|
209
|
+
draw_pic(sbar_x, y, armor_icon) if @textures[armor_icon]
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def weapon_flash_variant(ps, weapon, index, time)
|
|
213
|
+
gettime = ps.weapon_gettime[index].to_f
|
|
214
|
+
flashon = [((time.to_f - gettime) * 10.0).to_i, 0].max
|
|
215
|
+
return "inva#{(flashon % 5) + 1}" if flashon < 10
|
|
216
|
+
|
|
217
|
+
ps.current_weapon == weapon ? "inv2" : "inv"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def draw_inventory_ammo(ps)
|
|
221
|
+
INVENTORY_AMMO_TYPES.each_with_index do |ammo_type, index|
|
|
222
|
+
num = format("%3i", ps.ammo.fetch(ammo_type, 0))
|
|
223
|
+
3.times do |digit_index|
|
|
224
|
+
ch = num[digit_index]
|
|
225
|
+
next if ch == " "
|
|
226
|
+
|
|
227
|
+
# Sbar_DrawCharacter adds +4 to x (sbar.c)
|
|
228
|
+
x = sbar_x + (((6 * index) + digit_index + 1) * CHAR_SIZE) - 2 + 4
|
|
229
|
+
draw_character(x, SBAR_HEIGHT + 16, 18 + ch.ord - "0".ord)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def draw_inventory_items(ps, time: 0.0)
|
|
235
|
+
INVENTORY_ITEMS.each_with_index do |(type, item, icon), index|
|
|
236
|
+
next unless inventory_item_active?(ps, type, item, time)
|
|
237
|
+
|
|
238
|
+
draw_pic(sbar_x + 192 + (index * 16), SBAR_HEIGHT, icon)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def inventory_item_active?(ps, type, item, time)
|
|
243
|
+
case type
|
|
244
|
+
when :key
|
|
245
|
+
ps.keys_owned.include?(item)
|
|
246
|
+
when :powerup
|
|
247
|
+
powerup_item_active?(ps, item, time)
|
|
248
|
+
else
|
|
249
|
+
false
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def draw_sigils(ps)
|
|
254
|
+
4.times do |index|
|
|
255
|
+
next if (ps.serverflags & (1 << index)).zero?
|
|
256
|
+
|
|
257
|
+
draw_pic(sbar_x + 288 + (index * 8), SBAR_HEIGHT, "sb_sigil#{index + 1}")
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Sbar_SoloScoreboard (sbar.c:491): monster/secret counts, level
|
|
262
|
+
# time and level name drawn over the scorebar when the player is
|
|
263
|
+
# dead. C y offsets are from the top of the 24px sbar (top-down);
|
|
264
|
+
# in this file's bottom-up ortho a char at C offset y sits at
|
|
265
|
+
# GL y = SBAR_HEIGHT - y - CHAR_SIZE = 16 - y.
|
|
266
|
+
def draw_solo_scoreboard(stats)
|
|
267
|
+
draw_string(sbar_x + 8, 12,
|
|
268
|
+
format("Monsters:%3i /%3i",
|
|
269
|
+
stats[:monsters].to_i, stats[:total_monsters].to_i))
|
|
270
|
+
draw_string(sbar_x + 8, 4,
|
|
271
|
+
format("Secrets :%3i /%3i",
|
|
272
|
+
stats[:secrets].to_i, stats[:total_secrets].to_i))
|
|
273
|
+
|
|
274
|
+
total = stats[:time].to_f.to_i
|
|
275
|
+
minutes = total / 60
|
|
276
|
+
seconds = total - (60 * minutes)
|
|
277
|
+
draw_string(sbar_x + 184, 12,
|
|
278
|
+
format("Time :%3i:%i%i", minutes, seconds / 10, seconds % 10))
|
|
279
|
+
|
|
280
|
+
level_name = stats[:level_name].to_s
|
|
281
|
+
draw_string(sbar_x + 232 - (level_name.length * 4), 4, level_name)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Sbar_IntermissionOverlay (sbar.c:1193). C coords are top-down in
|
|
285
|
+
# the 320x200 virtual screen; converted here with
|
|
286
|
+
# GL y = VIRTUAL_HEIGHT - y - pic_height (big digits are 24 tall).
|
|
287
|
+
def draw_intermission(stats)
|
|
288
|
+
ensure_intermission_pics
|
|
289
|
+
|
|
290
|
+
draw_pic_top(sbar_x + 64, 24, "complete")
|
|
291
|
+
draw_pic_top(sbar_x, 56, "inter")
|
|
292
|
+
|
|
293
|
+
# time
|
|
294
|
+
time_y = VIRTUAL_HEIGHT - 64 - DIGIT_WIDTH
|
|
295
|
+
completed = stats[:completed_time].to_f.to_i
|
|
296
|
+
minutes = completed / 60
|
|
297
|
+
seconds = completed - (60 * minutes)
|
|
298
|
+
draw_num(sbar_x + 160, time_y, minutes, 3)
|
|
299
|
+
draw_pic(sbar_x + 234, time_y, "num_colon")
|
|
300
|
+
draw_pic(sbar_x + 246, time_y, "num_#{seconds / 10}")
|
|
301
|
+
draw_pic(sbar_x + 266, time_y, "num_#{seconds % 10}")
|
|
302
|
+
|
|
303
|
+
secrets_y = VIRTUAL_HEIGHT - 104 - DIGIT_WIDTH
|
|
304
|
+
draw_num(sbar_x + 160, secrets_y, stats[:secrets].to_i, 3)
|
|
305
|
+
draw_pic(sbar_x + 232, secrets_y, "num_slash")
|
|
306
|
+
draw_num(sbar_x + 240, secrets_y, stats[:total_secrets].to_i, 3)
|
|
307
|
+
|
|
308
|
+
monsters_y = VIRTUAL_HEIGHT - 144 - DIGIT_WIDTH
|
|
309
|
+
draw_num(sbar_x + 160, monsters_y, stats[:monsters].to_i, 3)
|
|
310
|
+
draw_pic(sbar_x + 232, monsters_y, "num_slash")
|
|
311
|
+
draw_num(sbar_x + 240, monsters_y, stats[:total_monsters].to_i, 3)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Draw a pic whose y is given top-down (like Quake's Draw_Pic).
|
|
315
|
+
def draw_pic_top(x, y, name)
|
|
316
|
+
tex = @textures[name]
|
|
317
|
+
return unless tex
|
|
318
|
+
|
|
319
|
+
draw_pic(x, VIRTUAL_HEIGHT - y - tex[:height], name)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Console notify lines (sprint messages) like Con_DrawNotify
|
|
323
|
+
# (console.c): up to four lines at the top left, x = 8, stacked
|
|
324
|
+
# 8px apart from the top of the screen.
|
|
325
|
+
def draw_notify_lines(lines)
|
|
326
|
+
Array(lines).each_with_index do |line, index|
|
|
327
|
+
draw_string(8, VIRTUAL_HEIGHT - ((index + 1) * CHAR_SIZE), line)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Centerprint text like SCR_DrawCenterString (screen.c): each line
|
|
332
|
+
# centered horizontally, the block starting 35% down the screen
|
|
333
|
+
# (or at fixed C y = 48 for messages taller than four lines).
|
|
334
|
+
def draw_centerprint(text)
|
|
335
|
+
text = text.to_s
|
|
336
|
+
return if text.empty?
|
|
337
|
+
|
|
338
|
+
lines = text.split("\n")
|
|
339
|
+
y = if lines.length <= 4
|
|
340
|
+
VIRTUAL_HEIGHT - (VIRTUAL_HEIGHT * 0.35).to_i
|
|
341
|
+
else
|
|
342
|
+
VIRTUAL_HEIGHT - 48
|
|
343
|
+
end
|
|
344
|
+
lines.each do |line|
|
|
345
|
+
draw_string((virtual_width - (line.length * CHAR_SIZE)) / 2, y, line)
|
|
346
|
+
y -= CHAR_SIZE
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Draw the '+' crosshair glyph at the center of the view, like
|
|
351
|
+
# Draw_Character(scr_vrect center, '+') in screen.c.
|
|
352
|
+
def draw_crosshair
|
|
353
|
+
draw_character((virtual_width / 2) - 4, (VIRTUAL_HEIGHT / 2) - 4, "+".ord)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def draw_character(x, y, num)
|
|
357
|
+
return if num == 32
|
|
358
|
+
|
|
359
|
+
tex = @textures["conchars"]
|
|
360
|
+
return unless tex
|
|
361
|
+
|
|
362
|
+
num &= 255
|
|
363
|
+
row = num >> 4
|
|
364
|
+
col = num & 15
|
|
365
|
+
step = 1.0 / 16.0
|
|
366
|
+
s = col * step
|
|
367
|
+
t = row * step
|
|
368
|
+
|
|
369
|
+
GL.BindTexture(GL::TEXTURE_2D, tex[:id])
|
|
370
|
+
GL.Begin(GL::QUADS)
|
|
371
|
+
GL.TexCoord2f(s, t); GL.Vertex2f(x, y + CHAR_SIZE)
|
|
372
|
+
GL.TexCoord2f(s + step, t); GL.Vertex2f(x + CHAR_SIZE, y + CHAR_SIZE)
|
|
373
|
+
GL.TexCoord2f(s + step, t + step); GL.Vertex2f(x + CHAR_SIZE, y)
|
|
374
|
+
GL.TexCoord2f(s, t + step); GL.Vertex2f(x, y)
|
|
375
|
+
GL.End
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Draw a string of console characters left-to-right, like
|
|
379
|
+
# Draw_String (each char CHAR_SIZE apart).
|
|
380
|
+
def draw_string(x, y, str)
|
|
381
|
+
str.to_s.each_char.with_index do |ch, i|
|
|
382
|
+
draw_character(x + (i * CHAR_SIZE), y, ch.ord)
|
|
383
|
+
end
|
|
120
384
|
end
|
|
121
385
|
|
|
122
386
|
# Draw a right-justified number using big digit graphics.
|
|
@@ -141,22 +405,36 @@ module Quake
|
|
|
141
405
|
ptr = str.length > digits ? str[str.length - digits..] : str
|
|
142
406
|
|
|
143
407
|
ptr.each_char do |ch|
|
|
408
|
+
prefix = red ? "anum_" : "num_"
|
|
144
409
|
if ch == "-"
|
|
145
|
-
draw_pic(x, y, "
|
|
410
|
+
draw_pic(x, y, "#{prefix}minus")
|
|
146
411
|
else
|
|
147
|
-
prefix = red ? "anum_" : "num_"
|
|
148
412
|
draw_pic(x, y, "#{prefix}#{ch}")
|
|
149
413
|
end
|
|
150
414
|
x += DIGIT_WIDTH
|
|
151
415
|
end
|
|
152
416
|
end
|
|
153
417
|
|
|
154
|
-
def health_face(
|
|
418
|
+
def health_face(player_state, time: 0.0)
|
|
419
|
+
if powerup_item_active?(player_state, :ring, time) &&
|
|
420
|
+
powerup_item_active?(player_state, :pentagram, time)
|
|
421
|
+
return "face_inv2"
|
|
422
|
+
end
|
|
423
|
+
return "face_quad" if powerup_item_active?(player_state, :quad, time)
|
|
424
|
+
return "face_invis" if powerup_item_active?(player_state, :ring, time)
|
|
425
|
+
return "face_invul2" if powerup_item_active?(player_state, :pentagram, time)
|
|
426
|
+
|
|
427
|
+
health = player_state.health
|
|
155
428
|
# Quake face mapping: face1 = best (health 100+), face5 = worst (near death)
|
|
156
429
|
# sb_faces[4] = face1, sb_faces[0] = face5
|
|
157
430
|
# f = health >= 100 ? 4 : health / 20
|
|
158
431
|
f = health >= 100 ? 4 : [health / 20, 0].max
|
|
159
|
-
"
|
|
432
|
+
prefix = time.to_f <= player_state.face_anim_time.to_f ? "face_p" : "face"
|
|
433
|
+
"#{prefix}#{5 - f}"
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def powerup_item_active?(player_state, powerup, time)
|
|
437
|
+
player_state.powerup_item_active?(powerup, game_time: time)
|
|
160
438
|
end
|
|
161
439
|
|
|
162
440
|
def ammo_type_icon(ammo_type)
|
|
@@ -169,8 +447,12 @@ module Quake
|
|
|
169
447
|
end
|
|
170
448
|
|
|
171
449
|
def upload_hud_graphics
|
|
450
|
+
upload_charset
|
|
451
|
+
|
|
172
452
|
# Status bar background
|
|
173
453
|
upload_wad_pic("sbar")
|
|
454
|
+
upload_wad_pic("ibar")
|
|
455
|
+
upload_wad_pic("scorebar")
|
|
174
456
|
|
|
175
457
|
# Big numbers (0-9) and alternate (red) numbers
|
|
176
458
|
10.times do |i|
|
|
@@ -178,14 +460,20 @@ module Quake
|
|
|
178
460
|
upload_wad_pic("anum_#{i}")
|
|
179
461
|
end
|
|
180
462
|
upload_wad_pic("num_minus")
|
|
463
|
+
upload_wad_pic("anum_minus")
|
|
181
464
|
upload_wad_pic("num_colon")
|
|
182
465
|
upload_wad_pic("num_slash")
|
|
183
466
|
|
|
184
467
|
# Face graphics
|
|
185
468
|
(1..5).each { |i| upload_wad_pic("face#{i}") }
|
|
186
469
|
(1..5).each { |i| upload_wad_pic("face_p#{i}") }
|
|
470
|
+
upload_wad_pic("face_invis")
|
|
471
|
+
upload_wad_pic("face_invul2")
|
|
472
|
+
upload_wad_pic("face_inv2")
|
|
473
|
+
upload_wad_pic("face_quad")
|
|
187
474
|
|
|
188
475
|
# Armor icons
|
|
476
|
+
upload_wad_pic("disc")
|
|
189
477
|
upload_wad_pic("sb_armor1")
|
|
190
478
|
upload_wad_pic("sb_armor2")
|
|
191
479
|
upload_wad_pic("sb_armor3")
|
|
@@ -196,6 +484,15 @@ module Quake
|
|
|
196
484
|
upload_wad_pic("sb_rocket")
|
|
197
485
|
upload_wad_pic("sb_cells")
|
|
198
486
|
|
|
487
|
+
WEAPON_ICONS.each do |_weapon, icon|
|
|
488
|
+
upload_wad_pic("inv_#{icon}")
|
|
489
|
+
upload_wad_pic("inv2_#{icon}")
|
|
490
|
+
(1..5).each { |i| upload_wad_pic("inva#{i}_#{icon}") }
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
INVENTORY_ITEMS.each { |_type, _item, icon| upload_wad_pic(icon) }
|
|
494
|
+
(1..4).each { |i| upload_wad_pic("sb_sigil#{i}") }
|
|
495
|
+
|
|
199
496
|
count = @textures.size
|
|
200
497
|
puts "Loaded #{count} HUD graphics from WAD"
|
|
201
498
|
end
|
|
@@ -204,7 +501,63 @@ module Quake
|
|
|
204
501
|
qpic = @wad.read_qpic(name)
|
|
205
502
|
return unless qpic
|
|
206
503
|
|
|
207
|
-
|
|
504
|
+
upload_texture(name, qpic.width, qpic.height, qpic.pixels)
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# gfx/complete.lmp and gfx/inter.lmp are standalone qpic pak
|
|
508
|
+
# entries (not in gfx.wad), loaded on first intermission and
|
|
509
|
+
# skipped tolerantly if the pak lacks them.
|
|
510
|
+
def ensure_intermission_pics
|
|
511
|
+
return if @intermission_pics_loaded
|
|
512
|
+
|
|
513
|
+
@intermission_pics_loaded = true
|
|
514
|
+
upload_pak_lmp("complete", "gfx/complete.lmp")
|
|
515
|
+
upload_pak_lmp("inter", "gfx/inter.lmp")
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# Standalone qpic lump: int32 width, int32 height, then
|
|
519
|
+
# width*height palette-indexed bytes.
|
|
520
|
+
def upload_pak_lmp(name, path)
|
|
521
|
+
return unless @pak
|
|
522
|
+
|
|
523
|
+
data = @pak.read(path)
|
|
524
|
+
return unless data && data.bytesize > 8
|
|
525
|
+
|
|
526
|
+
width, height = data[0, 8].unpack("VV")
|
|
527
|
+
return unless width.positive? && height.positive?
|
|
528
|
+
return if data.bytesize < 8 + (width * height)
|
|
529
|
+
|
|
530
|
+
upload_texture(name, width, height, data[8, width * height])
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def upload_texture(name, width, height, pixels, transparent_index: 255)
|
|
534
|
+
rgba = @palette.indexed_to_rgba(pixels, transparent_index: transparent_index)
|
|
535
|
+
|
|
536
|
+
buf = "\0" * 4
|
|
537
|
+
GL.GenTextures(1, buf)
|
|
538
|
+
tex_id = buf.unpack1("V")
|
|
539
|
+
|
|
540
|
+
GL.BindTexture(GL::TEXTURE_2D, tex_id)
|
|
541
|
+
GL.TexImage2D(GL::TEXTURE_2D, 0, GL::RGBA,
|
|
542
|
+
width, height, 0,
|
|
543
|
+
GL::RGBA, GL::UNSIGNED_BYTE, rgba)
|
|
544
|
+
# Quake filters sbar pics with gl_filter_max (GL_LINEAR); only the
|
|
545
|
+
# charset uses GL_NEAREST (gl_draw.c)
|
|
546
|
+
GL.TexParameteri(GL::TEXTURE_2D, GL::TEXTURE_MIN_FILTER, GL::LINEAR)
|
|
547
|
+
GL.TexParameteri(GL::TEXTURE_2D, GL::TEXTURE_MAG_FILTER, GL::LINEAR)
|
|
548
|
+
GL.TexParameteri(GL::TEXTURE_2D, GL::TEXTURE_WRAP_S, GL::CLAMP_TO_EDGE)
|
|
549
|
+
GL.TexParameteri(GL::TEXTURE_2D, GL::TEXTURE_WRAP_T, GL::CLAMP_TO_EDGE)
|
|
550
|
+
|
|
551
|
+
@textures[name] = { id: tex_id, width: width, height: height }
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def upload_charset
|
|
555
|
+
return unless @wad.list.include?("conchars")
|
|
556
|
+
|
|
557
|
+
# Quake makes only palette index 0 transparent in the charset
|
|
558
|
+
# (GL_LoadTexture_Alpha with alpha=0 in gl_draw.c)
|
|
559
|
+
pixels = @wad.read("conchars")
|
|
560
|
+
rgba = @palette.indexed_to_rgba(pixels, transparent_index: 0)
|
|
208
561
|
|
|
209
562
|
buf = "\0" * 4
|
|
210
563
|
GL.GenTextures(1, buf)
|
|
@@ -212,14 +565,14 @@ module Quake
|
|
|
212
565
|
|
|
213
566
|
GL.BindTexture(GL::TEXTURE_2D, tex_id)
|
|
214
567
|
GL.TexImage2D(GL::TEXTURE_2D, 0, GL::RGBA,
|
|
215
|
-
|
|
568
|
+
128, 128, 0,
|
|
216
569
|
GL::RGBA, GL::UNSIGNED_BYTE, rgba)
|
|
217
570
|
GL.TexParameteri(GL::TEXTURE_2D, GL::TEXTURE_MIN_FILTER, GL::NEAREST)
|
|
218
571
|
GL.TexParameteri(GL::TEXTURE_2D, GL::TEXTURE_MAG_FILTER, GL::NEAREST)
|
|
219
572
|
GL.TexParameteri(GL::TEXTURE_2D, GL::TEXTURE_WRAP_S, GL::CLAMP_TO_EDGE)
|
|
220
573
|
GL.TexParameteri(GL::TEXTURE_2D, GL::TEXTURE_WRAP_T, GL::CLAMP_TO_EDGE)
|
|
221
574
|
|
|
222
|
-
@textures[
|
|
575
|
+
@textures["conchars"] = { id: tex_id, width: 128, height: 128 }
|
|
223
576
|
end
|
|
224
577
|
end
|
|
225
578
|
end
|