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.
@@ -8,6 +8,8 @@ module Quake
8
8
  # entities, applies effects to PlayerState, marks items as picked up.
9
9
  class ItemPickups
10
10
  PICKUP_RADIUS = 32.0 # units (roughly player bbox width)
11
+ PLAYER_MINS = Math::Vec3.new(-16.0, -16.0, -24.0)
12
+ PLAYER_MAXS = Math::Vec3.new(16.0, 16.0, 32.0)
11
13
 
12
14
  # Pickup definitions: classname -> { type:, ... }
13
15
  ITEMS = {
@@ -20,10 +22,11 @@ module Quake
20
22
  "item_armorInv" => { type: :armor, points: 200, armor_type: 3 }, # red
21
23
 
22
24
  # Ammo
23
- "item_shells" => { type: :ammo, ammo_type: :shells, amount: 20 },
24
- "item_spikes" => { type: :ammo, ammo_type: :nails, amount: 25 },
25
- "item_rockets" => { type: :ammo, ammo_type: :rockets, amount: 5 },
26
- "item_cells" => { type: :ammo, ammo_type: :cells, amount: 6 },
25
+ "item_shells" => { type: :ammo, ammo_type: :shells, amount: 20, big_amount: 40 },
26
+ "item_spikes" => { type: :ammo, ammo_type: :nails, amount: 25, big_amount: 50 },
27
+ "item_rockets" => { type: :ammo, ammo_type: :rockets, amount: 5, big_amount: 10 },
28
+ "item_cells" => { type: :ammo, ammo_type: :cells, amount: 6, big_amount: 12 },
29
+ "item_weapon" => { type: :legacy_weapon_ammo },
27
30
 
28
31
  # Weapons (give weapon + starting ammo)
29
32
  "weapon_supershotgun" => {
@@ -55,36 +58,149 @@ module Quake
55
58
  "item_artifact_super_damage" => { type: :powerup, powerup: :quad },
56
59
  "item_artifact_invulnerability" => { type: :powerup, powerup: :pentagram },
57
60
  "item_artifact_envirosuit" => { type: :powerup, powerup: :biosuit },
58
- "item_artifact_invisibility" => { type: :powerup, powerup: :ring }
61
+ "item_artifact_invisibility" => { type: :powerup, powerup: :ring },
62
+
63
+ # Keys
64
+ "item_key1" => { type: :key, key: :silver },
65
+ "item_key2" => { type: :key, key: :gold },
66
+
67
+ # Episode runes
68
+ "item_sigil" => { type: :sigil },
69
+
70
+ # Dropped by killed players in QuakeC DropBackpack.
71
+ "item_backpack" => { type: :backpack }
72
+ }.freeze
73
+
74
+ BACKPACK_WEAPONS = {
75
+ 1 => :shotgun,
76
+ 2 => :super_shotgun,
77
+ 4 => :nailgun,
78
+ 8 => :super_nailgun,
79
+ 16 => :grenade_launcher,
80
+ 32 => :rocket_launcher,
81
+ 64 => :lightning_gun,
82
+ 4096 => :axe
83
+ }.freeze
84
+ SUB_REGEN_SOUND = "items/itembk2.wav"
85
+
86
+ # QuakeC items.qc netnames used by the "You got the ..." sprints.
87
+ WEAPON_NETNAMES = {
88
+ super_shotgun: "Double-barrelled Shotgun",
89
+ nailgun: "nailgun",
90
+ super_nailgun: "Super Nailgun",
91
+ grenade_launcher: "Grenade Launcher",
92
+ rocket_launcher: "Rocket Launcher",
93
+ lightning_gun: "Thunderbolt"
94
+ }.freeze
95
+
96
+ AMMO_NETNAMES = {
97
+ shells: "shells",
98
+ nails: "nails",
99
+ rockets: "rockets",
100
+ cells: "cells"
101
+ }.freeze
102
+
103
+ # Legacy item_weapon boxes use "spikes" for nails (items.qc).
104
+ LEGACY_AMMO_NETNAMES = {
105
+ shells: "shells",
106
+ nails: "spikes",
107
+ rockets: "rockets"
108
+ }.freeze
109
+
110
+ POWERUP_NETNAMES = {
111
+ quad: "Quad Damage",
112
+ pentagram: "Pentagram of Protection",
113
+ ring: "Ring of Shadows",
114
+ biosuit: "Biosuit"
115
+ }.freeze
116
+
117
+ KEY_NETNAMES = {
118
+ silver: "silver key",
119
+ gold: "gold key"
59
120
  }.freeze
60
121
 
61
- def initialize(entities)
122
+ # items.qc BackpackTouch netnames (set in DropBackpack spawns).
123
+ BACKPACK_WEAPON_NETNAMES = {
124
+ axe: "Axe",
125
+ shotgun: "Shotgun",
126
+ super_shotgun: "Double-barrelled Shotgun",
127
+ nailgun: "Nailgun",
128
+ super_nailgun: "Super Nailgun",
129
+ grenade_launcher: "Grenade Launcher",
130
+ rocket_launcher: "Rocket Launcher",
131
+ lightning_gun: "Thunderbolt"
132
+ }.freeze
133
+
134
+ def initialize(entities, deathmatch: 0)
62
135
  @item_entities = entities.select { |e| ITEMS.key?(e.classname) }
63
136
  @picked_up = Set.new # entity object_ids that have been collected
137
+ @deathmatch = deathmatch.to_i
138
+ end
139
+
140
+ def add_entity(ent)
141
+ @item_entities << ent if ITEMS.key?(ent.classname)
142
+ end
143
+
144
+ def update(game_time, player_state: nil)
145
+ events = []
146
+ @item_entities.each do |ent|
147
+ next if ent.removed?
148
+
149
+ if ent.instance_variable_get(:@think) == :sub_remove && ent.think_time <= game_time
150
+ ent.instance_variable_set(:@removed, true)
151
+ next
152
+ end
153
+
154
+ next unless @picked_up.include?(ent.object_id)
155
+ next if ent.think_time > game_time
156
+
157
+ case ent.instance_variable_get(:@think)
158
+ when :sub_regen
159
+ regen_item(ent)
160
+ events << { classname: ent.classname, type: :respawn, entity: ent, sound: SUB_REGEN_SOUND }
161
+ when :item_megahealth_rot
162
+ update_megahealth_item(ent, game_time)
163
+ end
164
+ end
165
+ events
64
166
  end
65
167
 
66
168
  # Check for pickups near player position. Returns array of pickup
67
169
  # event hashes (for sound/effect triggering).
68
- def check_pickups(player_pos, player_state)
170
+ def check_pickups(player_pos, player_state, game_time: 0.0, water_level: 0)
171
+ return [] unless player_state.alive?
172
+
69
173
  events = []
70
174
 
71
175
  @item_entities.each do |ent|
176
+ next if ent.removed?
72
177
  next if @picked_up.include?(ent.object_id)
178
+ next if ent.instance_variable_get(:@solid) == 0
73
179
 
74
- dx = player_pos.x - ent.position.x
75
- dy = player_pos.y - ent.position.y
76
- dz = player_pos.z - ent.position.z
77
- dist_sq = dx * dx + dy * dy + dz * dz
78
-
79
- next if dist_sq > PICKUP_RADIUS * PICKUP_RADIUS
180
+ next unless touches_item?(player_pos, ent)
80
181
 
81
182
  defn = ITEMS[ent.classname]
82
183
  next unless defn
83
184
 
84
- picked = try_pickup(player_state, defn, ent)
185
+ # Built before try_pickup mutates PlayerState (QuakeC prints
186
+ # from pre-pickup state, e.g. BackpackTouch weapon ownership).
187
+ message = pickup_message(defn, ent, player_state)
188
+ picked = try_pickup(player_state, defn, ent, game_time: game_time, water_level: water_level)
85
189
  if picked
86
- @picked_up.add(ent.object_id)
87
- events << { classname: ent.classname, type: defn[:type], entity: ent }
190
+ leaves_item = leave_weapon_pickup?(defn)
191
+ unless leaves_item
192
+ @picked_up.add(ent.object_id)
193
+ hide_picked_item(ent, defn, game_time)
194
+ end
195
+ events << {
196
+ classname: ent.classname,
197
+ type: defn[:type],
198
+ entity: ent,
199
+ sound: pickup_sound(defn, ent),
200
+ extra_sounds: ent.instance_variable_get(:@extra_pickup_sounds),
201
+ use_targets: !leaves_item,
202
+ message: message
203
+ }
88
204
  end
89
205
  end
90
206
 
@@ -95,43 +211,478 @@ module Quake
95
211
  @picked_up.include?(entity.object_id)
96
212
  end
97
213
 
214
+ def owns_megahealth_rot_for?(player_state)
215
+ @item_entities.any? do |ent|
216
+ @picked_up.include?(ent.object_id) &&
217
+ ent.instance_variable_get(:@think) == :item_megahealth_rot &&
218
+ ent.instance_variable_get(:@owner).equal?(player_state)
219
+ end
220
+ end
221
+
222
+ def hide_picked_item(ent, defn, game_time)
223
+ if defn[:type] == :backpack
224
+ ent.instance_variable_set(:@removed, true)
225
+ else
226
+ ent.instance_variable_set(:@mdl, ent["model"]) unless ent.instance_variable_get(:@mdl)
227
+ ent.properties["model"] = ""
228
+ ent.instance_variable_set(:@solid, 0)
229
+ return if dropped_powerup?(ent, defn)
230
+
231
+ schedule_regen(ent, defn, game_time)
232
+ end
233
+ end
234
+
98
235
  private
99
236
 
100
- def try_pickup(ps, defn, ent)
237
+ # SV_LinkEdict expands FL_ITEM entities' trigger bounds by 15 units
238
+ # in x/y so items are easier to grab.
239
+ FL_ITEM_EXPANSION = Math::Vec3.new(15.0, 15.0, 0.0)
240
+
241
+ def touches_item?(player_pos, ent)
242
+ mins = ent.instance_variable_get(:@mins)
243
+ maxs = ent.instance_variable_get(:@maxs)
244
+ return radius_touch?(player_pos, ent.position) unless mins && maxs
245
+
246
+ boxes_overlap?(player_pos + PLAYER_MINS, player_pos + PLAYER_MAXS,
247
+ ent.position + mins - FL_ITEM_EXPANSION,
248
+ ent.position + maxs + FL_ITEM_EXPANSION)
249
+ end
250
+
251
+ def radius_touch?(player_pos, item_pos)
252
+ dx = player_pos.x - item_pos.x
253
+ dy = player_pos.y - item_pos.y
254
+ dz = player_pos.z - item_pos.z
255
+ (dx * dx + dy * dy + dz * dz) <= PICKUP_RADIUS * PICKUP_RADIUS
256
+ end
257
+
258
+ def boxes_overlap?(a_mins, a_maxs, b_mins, b_maxs)
259
+ a_mins.x <= b_maxs.x && a_maxs.x >= b_mins.x &&
260
+ a_mins.y <= b_maxs.y && a_maxs.y >= b_mins.y &&
261
+ a_mins.z <= b_maxs.z && a_maxs.z >= b_mins.z
262
+ end
263
+
264
+ def regen_item(ent)
265
+ ent.properties["model"] = ent.instance_variable_get(:@mdl).to_s
266
+ ent.instance_variable_set(:@solid, 1)
267
+ ent.think_time = 0.0
268
+ ent.instance_variable_set(:@respawn_sound, SUB_REGEN_SOUND)
269
+ @picked_up.delete(ent.object_id)
270
+ end
271
+
272
+ def update_megahealth_item(ent, game_time)
273
+ owner = ent.instance_variable_get(:@owner)
274
+ if owner && owner.health > PlayerState::MAX_HEALTH
275
+ owner.health -= 1
276
+ ent.think_time = game_time.to_f + 1.0
277
+ else
278
+ if silly_old_deathmatch?
279
+ ent.instance_variable_set(:@think, nil)
280
+ ent.think_time = 0.0
281
+ else
282
+ ent.instance_variable_set(:@think, :sub_regen)
283
+ ent.think_time = game_time.to_f + 20.0
284
+ end
285
+ end
286
+ end
287
+
288
+ def schedule_regen(ent, defn, game_time)
289
+ delay, think = regen_delay_and_think(ent, defn)
290
+ return unless delay
291
+
292
+ ent.think_time = game_time.to_f + delay
293
+ ent.instance_variable_set(:@think, think)
294
+ end
295
+
296
+ def regen_delay_and_think(ent, defn)
101
297
  case defn[:type]
102
298
  when :health
103
- # Health items: check spawnflags for mega/rotten
104
- amount = defn[:amount]
105
- mega = false
106
- if (flags = ent["spawnflags"])
107
- flags = flags.to_i
108
- if flags & 1 != 0 # rotten (small health, 15hp, can exceed 100 but not by much)
109
- amount = 15
110
- elsif flags & 2 != 0 # mega health
111
- amount = 100
112
- mega = true
113
- end
299
+ if health_mega?(ent)
300
+ return [nil, nil] if deathmatch_four?
301
+
302
+ [5.0, :item_megahealth_rot]
303
+ else
304
+ return [nil, nil] if silly_old_deathmatch?
305
+
306
+ [20.0, :sub_regen]
114
307
  end
115
- ps.add_health(amount, mega: mega)
308
+ when :armor
309
+ return [nil, nil] if silly_old_deathmatch?
310
+
311
+ [20.0, :sub_regen]
312
+ when :ammo, :legacy_weapon_ammo
313
+ return [nil, nil] if silly_old_deathmatch?
314
+
315
+ [deathmatch_fast_ammo_respawn? ? 15.0 : 30.0, :sub_regen]
316
+ when :weapon
317
+ [30.0, :sub_regen]
318
+ when :powerup
319
+ if %w[item_artifact_invulnerability item_artifact_invisibility].include?(ent.classname)
320
+ [300.0, :sub_regen]
321
+ else
322
+ [60.0, :sub_regen]
323
+ end
324
+ end
325
+ end
326
+
327
+ def deathmatch_fast_ammo_respawn?
328
+ @deathmatch == 3 || @deathmatch == 5
329
+ end
330
+
331
+ def silly_old_deathmatch?
332
+ @deathmatch == 2
333
+ end
334
+
335
+ def deathmatch_four?
336
+ @deathmatch == 4
337
+ end
338
+
339
+ def clear_deathmatch_four_quad_resources(ps, ent)
340
+ return unless deathmatch_four?
341
+ return unless ent.classname == "item_artifact_super_damage"
342
+
343
+ ps.armor_type = 0
344
+ ps.armor = 0
345
+ ps.ammo[:cells] = 0
346
+ end
347
+
348
+ def leave_weapon_pickup?(defn)
349
+ defn[:type] == :weapon && [2, 3, 5].include?(@deathmatch)
350
+ end
351
+
352
+ def health_mega?(ent)
353
+ healtype = ent.instance_variable_get(:@healtype)
354
+ return healtype == 2 unless healtype.nil?
355
+
356
+ (ent.spawnflags & 2) != 0
357
+ end
358
+
359
+ def try_pickup(ps, defn, ent, game_time:, water_level:)
360
+ case defn[:type]
361
+ when :health
362
+ return false if deathmatch_four_invincible_player?(ps, game_time)
363
+
364
+ amount, mega = health_pickup_fields(defn, ent)
365
+ picked = ps.add_health(amount, mega: mega, game_time: deathmatch_four? ? nil : game_time)
366
+ ent.instance_variable_set(:@owner, ps) if picked && mega
367
+ picked
116
368
 
117
369
  when :armor
370
+ return false if deathmatch_four_invincible_player?(ps, game_time)
371
+
118
372
  ps.add_armor(defn[:points], defn[:armor_type])
119
373
 
120
374
  when :ammo
121
- ps.add_ammo(defn[:ammo_type], defn[:amount])
375
+ amount = ammo_amount(defn, ent)
376
+ ps.add_ammo(ammo_type(defn, ent), amount, auto_switch: true, water_level: water_level)
377
+
378
+ when :legacy_weapon_ammo
379
+ ammo = legacy_weapon_ammo(ent)
380
+ return true unless ammo
381
+
382
+ ps.add_ammo(ammo[:type], ammo[:amount], auto_switch: true, water_level: water_level)
122
383
 
123
384
  when :weapon
385
+ return false if leave_weapon_pickup?(defn) && ps.weapons_owned.include?(defn[:weapon])
386
+
124
387
  ps.give_weapon(defn[:weapon],
125
388
  ammo_type: defn[:ammo_type],
126
- ammo_amount: defn[:ammo_amount])
389
+ ammo_amount: defn[:ammo_amount],
390
+ water_level: water_level,
391
+ game_time: game_time,
392
+ w_switch: ps.respond_to?(:w_switch) ? ps.w_switch : 8)
127
393
 
128
394
  when :powerup
129
- # Powerups always pick up (full implementation would add timed effects)
130
- true
395
+ picked = ps.add_powerup(powerup_type(defn, ent), game_time: game_time, finish_time: recovered_powerup_finish_time(ent))
396
+ clear_deathmatch_four_quad_resources(ps, ent) if picked
397
+ picked
398
+
399
+ when :key
400
+ ps.add_key(key_type(defn, ent))
401
+
402
+ when :sigil
403
+ picked = ps.add_sigil(ent.spawnflags)
404
+ if picked
405
+ ps.instance_variable_set(:@centerprint, "You got the rune!")
406
+ ent.classname = ""
407
+ end
408
+ picked
409
+
410
+ when :backpack
411
+ return pickup_deathmatch_four_backpack(ps, ent, game_time: game_time) if deathmatch_four?
412
+
413
+ picked = ps.add_backpack(
414
+ ammo: backpack_ammo(ent),
415
+ weapon: backpack_weapon(ent),
416
+ water_level: water_level,
417
+ game_time: game_time,
418
+ b_switch: ps.respond_to?(:b_switch) ? ps.b_switch : 8
419
+ )
420
+ apply_deathmatch_rocket_backpack_minimum(ps, ent) if picked
421
+ picked
131
422
  else
132
423
  false
133
424
  end
134
425
  end
426
+
427
+ def ammo_amount(defn, ent)
428
+ aflag = ent.instance_variable_get(:@aflag)
429
+ return aflag if aflag
430
+
431
+ flags = (ent["spawnflags"] || "0").to_i
432
+ if flags & 1 != 0
433
+ defn.fetch(:big_amount, defn[:amount])
434
+ else
435
+ defn[:amount]
436
+ end
437
+ end
438
+
439
+ def deathmatch_four_invincible_player?(ps, game_time)
440
+ deathmatch_four? &&
441
+ (ps.powerup_item_active?(:pentagram, game_time: game_time) ||
442
+ ps.powerup_warning_time.key?(:pentagram))
443
+ end
444
+
445
+ def ammo_type(defn, ent)
446
+ weapon = ent.instance_variable_get(:@weapon)
447
+ case weapon
448
+ when 1 then :shells
449
+ when 2 then :nails
450
+ when 3 then :rockets
451
+ when 4 then :cells
452
+ else defn[:ammo_type]
453
+ end
454
+ end
455
+
456
+ def key_type(defn, ent)
457
+ case ent.instance_variable_get(:@items)
458
+ when 131_072 then :silver
459
+ when 262_144 then :gold
460
+ else defn[:key]
461
+ end
462
+ end
463
+
464
+ def powerup_type(defn, ent)
465
+ return :quad if ent.instance_variable_get(:@touch) == :q_touch
466
+ return :ring if ent.instance_variable_get(:@touch) == :r_touch
467
+
468
+ defn[:powerup]
469
+ end
470
+
471
+ def powerup_finish_time(ent)
472
+ cnt = ent.instance_variable_get(:@cnt)
473
+ cnt&.positive? ? cnt : nil
474
+ end
475
+
476
+ def recovered_powerup_finish_time(ent)
477
+ dropped_powerup_touch?(ent) ? powerup_finish_time(ent) : nil
478
+ end
479
+
480
+ def dropped_powerup?(ent, defn)
481
+ defn[:type] == :powerup &&
482
+ dropped_powerup_touch?(ent) &&
483
+ ent.instance_variable_get(:@think) == :sub_remove &&
484
+ powerup_finish_time(ent)
485
+ end
486
+
487
+ def dropped_powerup_touch?(ent)
488
+ %i[q_touch r_touch].include?(ent.instance_variable_get(:@touch))
489
+ end
490
+
491
+ def health_pickup_fields(defn, ent)
492
+ healamount = ent.instance_variable_get(:@healamount)
493
+ healtype = ent.instance_variable_get(:@healtype)
494
+ return [healamount, healtype == 2] unless healamount.nil? || healtype.nil?
495
+
496
+ flags = (ent["spawnflags"] || "0").to_i
497
+ if flags & 1 != 0
498
+ [15, false]
499
+ elsif flags & 2 != 0
500
+ [100, true]
501
+ else
502
+ [defn[:amount], false]
503
+ end
504
+ end
505
+
506
+ def legacy_weapon_ammo(ent)
507
+ spawned = legacy_spawned_ammo(ent)
508
+ return spawned if spawned
509
+
510
+ flags = (ent["spawnflags"] || "0").to_i
511
+ big = (flags & 8) != 0
512
+ if (flags & 2) != 0
513
+ { type: :rockets, amount: big ? 10 : 5 }
514
+ elsif (flags & 4) != 0
515
+ { type: :nails, amount: big ? 40 : 20 }
516
+ elsif (flags & 1) != 0
517
+ { type: :shells, amount: big ? 40 : 20 }
518
+ end
519
+ end
520
+
521
+ def legacy_spawned_ammo(ent)
522
+ weapon = ent.instance_variable_get(:@weapon)
523
+ amount = ent.instance_variable_get(:@aflag)
524
+ return unless weapon && amount
525
+
526
+ type = case weapon
527
+ when 1 then :shells
528
+ when 2 then :nails
529
+ when 3 then :rockets
530
+ end
531
+ type && { type: type, amount: amount }
532
+ end
533
+
534
+ # QuakeC items.qc pickup sprints ("You receive 25 health",
535
+ # "You got the nailgun", ...). Sigils use a centerprint instead.
536
+ def pickup_message(defn, ent, ps)
537
+ case defn[:type]
538
+ when :health
539
+ amount, = health_pickup_fields(defn, ent)
540
+ "You receive #{amount} health"
541
+ when :armor
542
+ "You got armor"
543
+ when :ammo
544
+ "You got the #{AMMO_NETNAMES[ammo_type(defn, ent)]}"
545
+ when :legacy_weapon_ammo
546
+ ammo = legacy_weapon_ammo(ent)
547
+ ammo && "You got the #{LEGACY_AMMO_NETNAMES[ammo[:type]]}"
548
+ when :weapon
549
+ "You got the #{WEAPON_NETNAMES[defn[:weapon]]}"
550
+ when :powerup
551
+ "You got the #{powerup_netname(defn, ent)}"
552
+ when :key
553
+ "You got the #{key_netname(defn, ent)}"
554
+ when :backpack
555
+ backpack_message(ent, ps)
556
+ end
557
+ end
558
+
559
+ def powerup_netname(defn, ent)
560
+ powerup = powerup_type(defn, ent)
561
+ return "OctaPower" if powerup == :quad && deathmatch_four?
562
+
563
+ ent.instance_variable_get(:@netname) || POWERUP_NETNAMES[powerup]
564
+ end
565
+
566
+ def key_netname(defn, ent)
567
+ ent.instance_variable_get(:@netname) || KEY_NETNAMES[key_type(defn, ent)]
568
+ end
569
+
570
+ # items.qc BackpackTouch: "You get " + optional "the <weapon>" +
571
+ # comma-separated ammo counts. Deathmatch 4 backpacks just give
572
+ # health.
573
+ def backpack_message(ent, ps)
574
+ return "You get 10 additional health" if deathmatch_four?
575
+
576
+ parts = []
577
+ weapon = backpack_weapon(ent)
578
+ if weapon && !ps.weapons_owned.include?(weapon)
579
+ parts << "the #{BACKPACK_WEAPON_NETNAMES[weapon]}"
580
+ end
581
+ backpack_ammo(ent).each do |type, amount|
582
+ parts << "#{amount} #{type}" if amount.positive?
583
+ end
584
+ "You get #{parts.join(', ')}"
585
+ end
586
+
587
+ def pickup_sound(defn, ent)
588
+ case defn[:type]
589
+ when :health
590
+ ent.instance_variable_get(:@noise) || health_sound(ent)
591
+ when :armor
592
+ "items/armor1.wav"
593
+ when :ammo, :legacy_weapon_ammo
594
+ "weapons/lock4.wav"
595
+ when :backpack
596
+ ent.instance_variable_get(:@noise) || "weapons/lock4.wav"
597
+ when :weapon
598
+ "weapons/pkup.wav"
599
+ when :powerup
600
+ ent.instance_variable_get(:@noise) || powerup_sound(defn[:powerup])
601
+ when :key
602
+ ent.instance_variable_get(:@noise) || "misc/medkey.wav"
603
+ when :sigil
604
+ ent.instance_variable_get(:@noise) || "misc/runekey.wav"
605
+ end
606
+ end
607
+
608
+ def health_sound(ent)
609
+ flags = (ent["spawnflags"] || "0").to_i
610
+ if flags & 1 != 0
611
+ "items/r_item1.wav"
612
+ elsif flags & 2 != 0
613
+ "items/r_item2.wav"
614
+ else
615
+ "items/health1.wav"
616
+ end
617
+ end
618
+
619
+ def powerup_sound(powerup)
620
+ case powerup
621
+ when :biosuit
622
+ "items/suit.wav"
623
+ when :pentagram
624
+ "items/protect.wav"
625
+ when :ring
626
+ "items/inv1.wav"
627
+ when :quad
628
+ "items/damage.wav"
629
+ end
630
+ end
631
+
632
+ def backpack_ammo(ent)
633
+ {
634
+ shells: backpack_field(ent, :ammo_shells),
635
+ nails: backpack_field(ent, :ammo_nails),
636
+ rockets: backpack_field(ent, :ammo_rockets),
637
+ cells: backpack_field(ent, :ammo_cells)
638
+ }
639
+ end
640
+
641
+ def backpack_weapon(ent)
642
+ BACKPACK_WEAPONS[backpack_field(ent, :items)]
643
+ end
644
+
645
+ def apply_deathmatch_rocket_backpack_minimum(ps, ent)
646
+ return unless deathmatch_fast_ammo_respawn?
647
+ item_bit = backpack_field(ent, :items)
648
+ rocket_weapon = if item_bit.zero?
649
+ %i[grenade_launcher rocket_launcher].include?(ps.current_weapon)
650
+ else
651
+ [16, 32].include?(item_bit)
652
+ end
653
+ return unless rocket_weapon
654
+ return unless ps.ammo[:rockets] < 5
655
+
656
+ ps.ammo[:rockets] = 5
657
+ end
658
+
659
+ def pickup_deathmatch_four_backpack(ps, ent, game_time:)
660
+ return false if deathmatch_four_invincible_player?(ps, game_time)
661
+
662
+ ps.health += 10
663
+ if ps.health > 250 && ps.health < 300
664
+ ent.instance_variable_set(:@noise, "items/protect3.wav")
665
+ else
666
+ ent.instance_variable_set(:@noise, "weapons/lock4.wav")
667
+ end
668
+
669
+ return true unless ps.health > 299
670
+ return true if deathmatch_four_invincible_player?(ps, game_time)
671
+
672
+ finish_time = game_time.to_f + 30.0
673
+ ps.add_powerup(:pentagram, game_time: game_time, finish_time: finish_time)
674
+ ps.add_powerup(:quad, game_time: game_time, finish_time: finish_time)
675
+ ps.ammo[:cells] = 0
676
+ ent.instance_variable_set(:@extra_pickup_sounds, ["boss1/sight1.wav"])
677
+ true
678
+ end
679
+
680
+ def backpack_field(ent, field)
681
+ value = ent.instance_variable_get(:"@#{field}")
682
+ return value.to_i if value
683
+
684
+ ent[field.to_s].to_i
685
+ end
135
686
  end
136
687
  end
137
688
  end