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
data/lib/quake/game/engine.rb
CHANGED
|
@@ -1,22 +1,98 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../debug/screenshot"
|
|
4
|
+
require_relative "../mdl/types"
|
|
5
|
+
require_relative "../mdl/reader"
|
|
6
|
+
require_relative "../renderer/gl_lightmap"
|
|
7
|
+
require_relative "../renderer/gl_view_blend"
|
|
4
8
|
|
|
5
9
|
module Quake
|
|
6
10
|
module Game
|
|
7
11
|
# Encapsulates the entire game state and per-frame loop, allowing both
|
|
8
12
|
# interactive (bin/quake) and scripted (bin/quake-debug) execution.
|
|
9
13
|
class Engine
|
|
14
|
+
POWERUP_EXPIRING_EVENTS = {
|
|
15
|
+
ring: :ring_expiring,
|
|
16
|
+
pentagram: :pentagram_expiring,
|
|
17
|
+
biosuit: :biosuit_expiring,
|
|
18
|
+
quad: :quad_expiring
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# CheckPowerups sprints (client.qc) shown when a powerup has less
|
|
22
|
+
# than three seconds left.
|
|
23
|
+
POWERUP_EXPIRING_MESSAGES = {
|
|
24
|
+
ring: "Ring of Shadows magic is fading",
|
|
25
|
+
pentagram: "Protection is almost burned out",
|
|
26
|
+
biosuit: "Air supply in Biosuit expiring",
|
|
27
|
+
quad: "Quad Damage is wearing off"
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
# scr_centertime (screen.c) and con_notifytime (console.c) defaults.
|
|
31
|
+
CENTERPRINT_TIME = 2.0
|
|
32
|
+
CON_NOTIFYTIME = 3.0
|
|
33
|
+
# NUM_CON_TIMES notify lines shown at once (console.c Con_DrawNotify).
|
|
34
|
+
MAX_NOTIFY_LINES = 4
|
|
35
|
+
|
|
36
|
+
START_ITEM_CLASSNAMES = %w[
|
|
37
|
+
item_health item_armor1 item_armor2 item_armorInv
|
|
38
|
+
weapon_supershotgun weapon_nailgun weapon_supernailgun
|
|
39
|
+
weapon_grenadelauncher weapon_rocketlauncher weapon_lightning
|
|
40
|
+
item_shells item_spikes item_rockets item_cells item_weapon
|
|
41
|
+
item_key1 item_key2 item_sigil
|
|
42
|
+
item_artifact_invulnerability item_artifact_envirosuit
|
|
43
|
+
item_artifact_invisibility item_artifact_super_damage
|
|
44
|
+
].freeze
|
|
45
|
+
|
|
46
|
+
ITEM_WIDE_BOUNDS_CLASSNAMES = %w[
|
|
47
|
+
item_health item_shells item_spikes item_rockets item_cells item_weapon
|
|
48
|
+
].freeze
|
|
49
|
+
|
|
50
|
+
ITEM_TALL_BOUNDS_CLASSNAMES = %w[
|
|
51
|
+
item_armor1 item_armor2 item_armorInv
|
|
52
|
+
weapon_supershotgun weapon_nailgun weapon_supernailgun
|
|
53
|
+
weapon_grenadelauncher weapon_rocketlauncher weapon_lightning
|
|
54
|
+
].freeze
|
|
55
|
+
|
|
56
|
+
QUAKEWORLD_REMOVED_MONSTER_CLASSNAMES = %w[
|
|
57
|
+
monster_ogre monster_demon1 monster_shambler monster_knight
|
|
58
|
+
monster_army monster_wizard monster_dog monster_zombie
|
|
59
|
+
monster_boss monster_tarbaby monster_hell_knight monster_fish
|
|
60
|
+
monster_shalrath monster_enforcer monster_oldone
|
|
61
|
+
].freeze
|
|
62
|
+
|
|
63
|
+
PLAYER_DEATH_FRAMES = {
|
|
64
|
+
axe: (41..49).to_a,
|
|
65
|
+
a: (50..60).to_a,
|
|
66
|
+
b: (61..69).to_a,
|
|
67
|
+
c: (70..84).to_a,
|
|
68
|
+
d: (85..93).to_a,
|
|
69
|
+
e: (94..102).to_a
|
|
70
|
+
}.freeze
|
|
71
|
+
PLAYER_DEATHA11_FRAME = 60
|
|
72
|
+
|
|
73
|
+
PLAYER_PAIN_FRAMES = {
|
|
74
|
+
axe: (29..34).to_a,
|
|
75
|
+
normal: (35..40).to_a
|
|
76
|
+
}.freeze
|
|
77
|
+
PLAYER_STAND_FRAMES = {
|
|
78
|
+
axe: (17..28).to_a,
|
|
79
|
+
normal: (12..16).to_a
|
|
80
|
+
}.freeze
|
|
81
|
+
|
|
10
82
|
attr_reader :pak, :palette, :wad, :window, :level, :entities,
|
|
11
83
|
:player, :player_state, :camera,
|
|
12
84
|
:viewmodel, :hud, :particles, :sound_events,
|
|
13
|
-
:brush_game, :item_pickups
|
|
14
|
-
attr_accessor :game_time
|
|
85
|
+
:brush_game, :item_pickups, :dynamic_lights
|
|
86
|
+
attr_accessor :game_time, :samelevel, :deathmatch, :teamplay, :rj, :axe, :temp1,
|
|
87
|
+
:timelimit, :fraglimit
|
|
88
|
+
attr_writer :attack_pressed
|
|
15
89
|
|
|
16
90
|
def initialize(pak_path:, window: nil, window_visible: true,
|
|
17
91
|
window_width: nil, window_height: nil,
|
|
18
|
-
enable_sound: true, enable_render: true
|
|
92
|
+
enable_sound: true, enable_render: true,
|
|
93
|
+
registered: false)
|
|
19
94
|
@enable_render = enable_render
|
|
95
|
+
@registered = registered
|
|
20
96
|
|
|
21
97
|
# PAK + assets
|
|
22
98
|
@pak = Pak::Reader.new(pak_path)
|
|
@@ -45,7 +121,57 @@ module Quake
|
|
|
45
121
|
end
|
|
46
122
|
|
|
47
123
|
@keys = {}
|
|
124
|
+
@attack_pressed = false
|
|
125
|
+
@attack_finished = 0.0
|
|
126
|
+
@pending_changelevel = nil
|
|
127
|
+
@pending_changelevel_time = nil
|
|
128
|
+
@samelevel = 0
|
|
129
|
+
@deathmatch = 0
|
|
130
|
+
@teamplay = 0
|
|
131
|
+
@rj = 1.0
|
|
132
|
+
@axe = 0
|
|
133
|
+
@temp1 = 0
|
|
134
|
+
@timelimit = 0
|
|
135
|
+
@fraglimit = 0
|
|
136
|
+
@current_map = nil
|
|
137
|
+
@killed_monsters = 0
|
|
138
|
+
@total_monsters = 0
|
|
139
|
+
@level_name = nil
|
|
140
|
+
@intermission_running = false
|
|
141
|
+
@intermission_start_time = nil
|
|
142
|
+
@gravity = GRAVITY
|
|
143
|
+
@projectiles = []
|
|
144
|
+
@temporary_dynamic_lights = []
|
|
145
|
+
@temporary_beams = []
|
|
146
|
+
@environment_damage_time = 0.0
|
|
147
|
+
@air_finished = 12.0
|
|
148
|
+
@drown_damage = 2.0
|
|
149
|
+
@drown_pain_finished = 0.0
|
|
150
|
+
@pain_sound_finished = 0.0
|
|
151
|
+
@invincible_sound_finished = 0.0
|
|
152
|
+
@swim_sound_finished = 0.0
|
|
153
|
+
@quad_attack_sound_finished = 0.0
|
|
154
|
+
@ring_idle_sound_finished = 0.0
|
|
155
|
+
@powerup_warning_finished = {}
|
|
156
|
+
@bonus_cshift_percent = 0.0
|
|
157
|
+
@damage_cshift = Renderer::GLViewBlend::CShift.new(color: [0, 0, 0], percent: 0.0)
|
|
158
|
+
@damage_kick_time = 0.0
|
|
159
|
+
@damage_kick_roll = 0.0
|
|
160
|
+
@damage_kick_pitch = 0.0
|
|
161
|
+
@weapon_punch_angle = Math::Vec3::ORIGIN
|
|
162
|
+
@view_oldz = 0.0
|
|
163
|
+
@view_frame_dt = 0.0
|
|
164
|
+
@view_idle_scale = 0.0
|
|
165
|
+
@view_idealpitch = 0.0
|
|
166
|
+
@view_pitch_velocity = 0.0
|
|
167
|
+
@view_pitch_nodrift = true
|
|
168
|
+
@view_pitch_driftmove = 0.0
|
|
169
|
+
@view_pitch_laststop = 0.0
|
|
170
|
+
@jump_flag = 0.0
|
|
48
171
|
@game_time = 0.0
|
|
172
|
+
@notify_lines = []
|
|
173
|
+
@centerprint = nil
|
|
174
|
+
@centerprint_expire = nil
|
|
49
175
|
end
|
|
50
176
|
|
|
51
177
|
# Load a BSP map and (re)build all per-level state.
|
|
@@ -54,37 +180,79 @@ module Quake
|
|
|
54
180
|
raise "Map not found: #{map_name}" unless bsp_data
|
|
55
181
|
|
|
56
182
|
@level = Bsp::Reader.new(bsp_data).parse
|
|
183
|
+
@current_map = map_name
|
|
184
|
+
configure_world_gravity(map_name)
|
|
57
185
|
@entities = EntityParser.parse(@level.entities)
|
|
58
186
|
@target_map = EntityParser.build_target_map(@entities)
|
|
187
|
+
initialize_body_queue
|
|
188
|
+
@projectiles = []
|
|
189
|
+
@temporary_dynamic_lights = []
|
|
190
|
+
@temporary_beams = []
|
|
191
|
+
@environment_damage_time = 0.0
|
|
192
|
+
@air_finished = @game_time + 12.0
|
|
193
|
+
@drown_damage = 2.0
|
|
194
|
+
@drown_pain_finished = 0.0
|
|
195
|
+
@player_in_water = false
|
|
196
|
+
@bonus_cshift_percent = 0.0
|
|
197
|
+
@damage_cshift = Renderer::GLViewBlend::CShift.new(color: [0, 0, 0], percent: 0.0)
|
|
198
|
+
@damage_kick_time = 0.0
|
|
199
|
+
@damage_kick_roll = 0.0
|
|
200
|
+
@damage_kick_pitch = 0.0
|
|
201
|
+
@weapon_punch_angle = Math::Vec3::ORIGIN
|
|
202
|
+
@view_oldz = 0.0
|
|
203
|
+
@view_frame_dt = 0.0
|
|
204
|
+
@view_idle_scale = 0.0
|
|
205
|
+
@view_idealpitch = 0.0
|
|
206
|
+
@view_pitch_velocity = 0.0
|
|
207
|
+
@view_pitch_nodrift = true
|
|
208
|
+
@view_pitch_driftmove = 0.0
|
|
209
|
+
@view_pitch_laststop = 0.0
|
|
210
|
+
@attack_finished = @game_time
|
|
211
|
+
@jump_flag = 0.0
|
|
212
|
+
@notify_lines = []
|
|
213
|
+
@centerprint = nil
|
|
214
|
+
@centerprint_expire = nil
|
|
215
|
+
setup_misc_entities
|
|
216
|
+
setup_ambient_sounds
|
|
217
|
+
|
|
218
|
+
# Level stats for the HUD scoreboard/intermission (Quake's
|
|
219
|
+
# total_monsters / killed_monsters, sbar.c STAT_* values).
|
|
220
|
+
# Monsters count only entities that actually spawned (the
|
|
221
|
+
# QuakeWorld-style monster stubs remove themselves at spawn).
|
|
222
|
+
@killed_monsters = 0
|
|
223
|
+
@total_monsters = @entities.count do |ent|
|
|
224
|
+
ent.classname.to_s.start_with?("monster_") && !ent.removed?
|
|
225
|
+
end
|
|
226
|
+
message = world_info_value("message")
|
|
227
|
+
@level_name = message && !message.empty? ? message : File.basename(map_name, ".bsp")
|
|
228
|
+
@intermission_start_time = nil
|
|
59
229
|
|
|
60
|
-
|
|
61
|
-
player_ent = @entities.find { |e| e.classname == "info_player_start" }
|
|
62
|
-
@player_start = player_ent&.position || Math::Vec3::ORIGIN
|
|
63
|
-
@player_yaw = player_ent&.angle || 0.0
|
|
230
|
+
set_player_spawn_point(select_spawn_point)
|
|
64
231
|
|
|
65
232
|
# Load MDL models referenced by entities
|
|
66
233
|
@mdl_cache ||= {}
|
|
67
|
-
@entities.each
|
|
68
|
-
|
|
69
|
-
next unless model_path&.end_with?(".mdl")
|
|
70
|
-
next if @mdl_cache.key?(model_path)
|
|
71
|
-
begin
|
|
72
|
-
mdl_data = @pak.read(model_path)
|
|
73
|
-
@mdl_cache[model_path] = Mdl::Reader.new(mdl_data).parse
|
|
74
|
-
rescue => e
|
|
75
|
-
warn "Failed to load #{model_path}: #{e.message}"
|
|
76
|
-
@mdl_cache[model_path] = nil
|
|
77
|
-
end
|
|
78
|
-
end
|
|
234
|
+
@entities.each { |ent| load_mdl_model(ent["model"]) unless ent.removed? }
|
|
235
|
+
TEMP_ENTITY_MODELS.each { |model_path| load_mdl_model(model_path) }
|
|
79
236
|
|
|
80
237
|
# Persistent player state across level loads
|
|
81
238
|
@player_state ||= PlayerState.new
|
|
82
|
-
@
|
|
83
|
-
|
|
239
|
+
@player_state.reset_spawn_runtime_fields
|
|
240
|
+
apply_start_map_parms
|
|
241
|
+
@item_pickups = ItemPickups.new(@entities, deathmatch: deathmatch)
|
|
242
|
+
@brush_game = BrushEntities.new(
|
|
243
|
+
@entities, @level, @target_map,
|
|
244
|
+
sound_events: @sound_events,
|
|
245
|
+
registered: @registered,
|
|
246
|
+
serverflags: @player_state.serverflags,
|
|
247
|
+
worldtype: current_worldtype
|
|
248
|
+
)
|
|
84
249
|
|
|
85
250
|
# Player + camera (reset position to start)
|
|
86
|
-
@player = Physics::Player.new(position:
|
|
87
|
-
@
|
|
251
|
+
@player = Physics::Player.new(position: player_spawn_origin, yaw: @player_yaw)
|
|
252
|
+
@player.gravity = current_gravity
|
|
253
|
+
set_player_spawn_entity_state
|
|
254
|
+
apply_put_client_in_server_spawn_effects
|
|
255
|
+
@camera = Camera.new(position: player_eye_position, yaw: @player_yaw)
|
|
88
256
|
|
|
89
257
|
build_renderers if @enable_render
|
|
90
258
|
end
|
|
@@ -93,10 +261,19 @@ module Quake
|
|
|
93
261
|
def tick(dt)
|
|
94
262
|
@game_time += dt
|
|
95
263
|
|
|
264
|
+
if @intermission_running
|
|
265
|
+
intermission_think
|
|
266
|
+
sync_camera
|
|
267
|
+
return unless @enable_render
|
|
268
|
+
|
|
269
|
+
render
|
|
270
|
+
return
|
|
271
|
+
end
|
|
272
|
+
|
|
96
273
|
# Brush entities first (so collision uses current positions)
|
|
97
274
|
if @brush_game
|
|
98
|
-
@brush_game.update(dt, @player.position)
|
|
99
|
-
@brush_game.check_triggers(@player.position) do |trigger|
|
|
275
|
+
@brush_game.update(dt, @player.position, player_state: @player_state)
|
|
276
|
+
@brush_game.check_triggers(@player.position, player_forward: @player.forward_flat) do |trigger|
|
|
100
277
|
handle_trigger(trigger)
|
|
101
278
|
end
|
|
102
279
|
|
|
@@ -109,14 +286,40 @@ module Quake
|
|
|
109
286
|
end
|
|
110
287
|
|
|
111
288
|
solid_brush = @entities.select(&:brush_entity?)
|
|
112
|
-
|
|
289
|
+
update_player_control(dt, solid_brush)
|
|
290
|
+
@player_state.update_megahealth(@game_time) unless @item_pickups&.owns_megahealth_rot_for?(@player_state)
|
|
291
|
+
update_start_items
|
|
292
|
+
if @item_pickups
|
|
293
|
+
@item_pickups.update(@game_time, player_state: @player_state).each do |evt|
|
|
294
|
+
@sound_events&.on_pickup(evt)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
113
297
|
|
|
114
298
|
# Item pickups
|
|
115
|
-
events = @item_pickups.check_pickups(
|
|
299
|
+
events = @item_pickups.check_pickups(
|
|
300
|
+
@player.position, @player_state, game_time: @game_time, water_level: @player.water_level
|
|
301
|
+
)
|
|
116
302
|
events.each do |evt|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
303
|
+
process_item_pickup_feedback(evt)
|
|
304
|
+
process_item_pickup_targets(evt)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
if player_postthink_active?
|
|
308
|
+
check_powerup_warnings
|
|
309
|
+
refresh_biosuit_air
|
|
310
|
+
update_player_powerup_visual_state
|
|
311
|
+
update_player_powerups
|
|
312
|
+
check_no_ammo_weapon_switch
|
|
313
|
+
fire_weapon if @attack_pressed && @game_time >= @attack_finished
|
|
314
|
+
end
|
|
315
|
+
update_triggered_misc_entities
|
|
316
|
+
update_projectiles(dt)
|
|
317
|
+
update_temporary_entities
|
|
318
|
+
update_player_pain_animation
|
|
319
|
+
update_player_death_animation
|
|
320
|
+
check_rules if player_postthink_active?
|
|
321
|
+
if @pending_changelevel && @game_time + 0.000001 >= @pending_changelevel_time.to_f
|
|
322
|
+
run_pending_changelevel
|
|
120
323
|
end
|
|
121
324
|
|
|
122
325
|
# Viewmodel bob (before camera so bob is applied to camera too)
|
|
@@ -125,48 +328,132 @@ module Quake
|
|
|
125
328
|
@viewmodel.update(dt, speed)
|
|
126
329
|
end
|
|
127
330
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
@camera.position = Math::Vec3.new(eye.x, eye.y, eye.z + bob)
|
|
132
|
-
@camera.yaw = @player.yaw
|
|
133
|
-
@camera.pitch = @player.pitch
|
|
331
|
+
@view_frame_dt = dt
|
|
332
|
+
sync_camera
|
|
333
|
+
update_screen_messages
|
|
134
334
|
|
|
135
335
|
return unless @enable_render
|
|
136
336
|
|
|
137
337
|
@world_renderer.update(dt) if @world_renderer.respond_to?(:update)
|
|
138
338
|
@particles&.update(dt)
|
|
339
|
+
update_view_cshifts(dt)
|
|
139
340
|
|
|
140
341
|
render
|
|
141
342
|
end
|
|
142
343
|
|
|
143
344
|
def render
|
|
144
|
-
|
|
145
|
-
@
|
|
345
|
+
build_dynamic_lights
|
|
346
|
+
@lightmap&.update(@game_time, lightstyles: current_lightstyles, dynamic_lights: @dynamic_lights)
|
|
347
|
+
@world_renderer.render(@camera, @window.aspect_ratio, time: @game_time)
|
|
348
|
+
@brush_renderer&.render(@entities, view_origin: @camera.position, time: @game_time,
|
|
349
|
+
frustum: @camera.frustum_planes(@window.aspect_ratio))
|
|
146
350
|
|
|
147
351
|
@entities.each do |ent|
|
|
352
|
+
next if ent.removed?
|
|
148
353
|
next if @item_pickups.picked_up?(ent)
|
|
354
|
+
|
|
355
|
+
emit_entity_effect_particles(ent)
|
|
149
356
|
model_path = ent["model"]
|
|
150
357
|
next unless model_path&.end_with?(".mdl")
|
|
151
358
|
|
|
152
359
|
gl_model = @mdl_renderers[model_path]
|
|
153
360
|
next unless gl_model
|
|
154
|
-
|
|
155
|
-
frame_rate = 10.0
|
|
156
|
-
frame = (@game_time * frame_rate).to_i
|
|
157
|
-
lerp = (@game_time * frame_rate) % 1.0
|
|
361
|
+
ambient_light, shade_light = alias_entity_lighting(ent.position)
|
|
158
362
|
|
|
159
363
|
gl_model.render(
|
|
160
|
-
frame_index: frame,
|
|
161
|
-
lerp:
|
|
364
|
+
frame_index: ent.frame,
|
|
365
|
+
lerp: 0.0,
|
|
162
366
|
position: ent.position,
|
|
163
|
-
yaw: ent
|
|
367
|
+
yaw: alias_entity_yaw(ent, model_path),
|
|
368
|
+
pitch: -ent.angles.x,
|
|
369
|
+
roll: ent.angles.z,
|
|
370
|
+
skin_index: ent.skin,
|
|
371
|
+
time: @game_time + entity_sync_base(ent, gl_model),
|
|
372
|
+
ambient_light: ambient_light,
|
|
373
|
+
shade_light: shade_light
|
|
164
374
|
)
|
|
165
375
|
end
|
|
166
376
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
@
|
|
377
|
+
render_projectiles
|
|
378
|
+
render_temp_beams
|
|
379
|
+
@particles&.render(camera: @camera)
|
|
380
|
+
render_viewmodel
|
|
381
|
+
@world_renderer.render_water
|
|
382
|
+
@view_blend&.render(current_view_blend)
|
|
383
|
+
@hud&.render(@player_state, time: @game_time, stats: hud_stats)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Level statistics for the HUD (Quake's cl.stats STAT_MONSTERS /
|
|
387
|
+
# STAT_SECRETS etc. plus intermission state, sbar.c).
|
|
388
|
+
def hud_stats
|
|
389
|
+
{
|
|
390
|
+
monsters: @killed_monsters.to_i,
|
|
391
|
+
total_monsters: @total_monsters.to_i,
|
|
392
|
+
secrets: @brush_game&.found_secrets.to_i,
|
|
393
|
+
total_secrets: @brush_game&.total_secrets.to_i,
|
|
394
|
+
time: @game_time,
|
|
395
|
+
level_name: @level_name,
|
|
396
|
+
intermission: !!@intermission_running,
|
|
397
|
+
completed_time: @intermission_start_time,
|
|
398
|
+
sb_lines: sb_lines,
|
|
399
|
+
centerprint: active_centerprint,
|
|
400
|
+
notify_lines: active_notify_lines
|
|
401
|
+
}
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# QuakeC sprint(): queue a console notify line (top-left text,
|
|
405
|
+
# console.c Con_Print + Con_DrawNotify). Multi-line messages become
|
|
406
|
+
# one notify line each; only the last MAX_NOTIFY_LINES are kept.
|
|
407
|
+
def sprint(text)
|
|
408
|
+
@notify_lines ||= []
|
|
409
|
+
text.to_s.split("\n").each do |line|
|
|
410
|
+
next if line.empty?
|
|
411
|
+
|
|
412
|
+
@notify_lines << { text: line, expire: @game_time + CON_NOTIFYTIME }
|
|
413
|
+
@notify_lines.shift while @notify_lines.length > MAX_NOTIFY_LINES
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Latch centerprints written to the player (QuakeC centerprint())
|
|
418
|
+
# and drop expired on-screen text. Runs once per tick.
|
|
419
|
+
def update_screen_messages
|
|
420
|
+
message = @player_state&.centerprint
|
|
421
|
+
if message && !message.to_s.empty?
|
|
422
|
+
@centerprint = message
|
|
423
|
+
@centerprint_expire = @game_time + CENTERPRINT_TIME
|
|
424
|
+
@player_state.centerprint = nil
|
|
425
|
+
end
|
|
426
|
+
@centerprint = nil if @centerprint && @centerprint_expire.to_f <= @game_time
|
|
427
|
+
@notify_lines&.reject! { |line| line[:expire] <= @game_time }
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def active_centerprint
|
|
431
|
+
return nil unless @centerprint
|
|
432
|
+
return nil if @centerprint_expire.to_f <= @game_time
|
|
433
|
+
|
|
434
|
+
@centerprint
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def active_notify_lines
|
|
438
|
+
(@notify_lines || []).select { |line| line[:expire] > @game_time }
|
|
439
|
+
.map { |line| line[:text] }
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# viewsize like Quake's +/- keys: 120 hides the bar, 110 shows the
|
|
443
|
+
# status bar only (default), 100 adds the inventory bar. Sub-100
|
|
444
|
+
# sizes (shrunk 3D view) are not supported.
|
|
445
|
+
def adjust_viewsize(delta)
|
|
446
|
+
@viewsize = ((@viewsize || DEFAULT_VIEWSIZE) + delta).clamp(100, 120)
|
|
447
|
+
puts "viewsize #{@viewsize}"
|
|
448
|
+
@viewsize
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def sb_lines
|
|
452
|
+
size = @viewsize || DEFAULT_VIEWSIZE
|
|
453
|
+
return 0 if size >= 120
|
|
454
|
+
return 24 if size >= 110
|
|
455
|
+
|
|
456
|
+
40
|
|
170
457
|
end
|
|
171
458
|
|
|
172
459
|
def swap_buffers
|
|
@@ -191,6 +478,10 @@ module Quake
|
|
|
191
478
|
@keys
|
|
192
479
|
end
|
|
193
480
|
|
|
481
|
+
def set_attack(pressed)
|
|
482
|
+
@attack_pressed = pressed
|
|
483
|
+
end
|
|
484
|
+
|
|
194
485
|
# ---------------------- player control ---------------------
|
|
195
486
|
|
|
196
487
|
def teleport(x, y, z, yaw: nil, pitch: nil)
|
|
@@ -198,9 +489,7 @@ module Quake
|
|
|
198
489
|
@player.velocity = Math::Vec3::ORIGIN
|
|
199
490
|
@player.instance_variable_set(:@yaw, yaw.to_f) if yaw
|
|
200
491
|
@player.instance_variable_set(:@pitch, pitch.to_f) if pitch
|
|
201
|
-
|
|
202
|
-
@camera.yaw = @player.yaw
|
|
203
|
-
@camera.pitch = @player.pitch
|
|
492
|
+
sync_camera
|
|
204
493
|
end
|
|
205
494
|
|
|
206
495
|
# ---------------------- debug output -----------------------
|
|
@@ -212,6 +501,7 @@ module Quake
|
|
|
212
501
|
def dump_state
|
|
213
502
|
{
|
|
214
503
|
time: @game_time,
|
|
504
|
+
map: @current_map,
|
|
215
505
|
player: {
|
|
216
506
|
position: vec_to_a(@player.position),
|
|
217
507
|
velocity: vec_to_a(@player.velocity),
|
|
@@ -226,7 +516,8 @@ module Quake
|
|
|
226
516
|
armor: @player_state.armor,
|
|
227
517
|
armor_type: @player_state.armor_type,
|
|
228
518
|
current_weapon: @player_state.current_weapon,
|
|
229
|
-
ammo: @player_state.ammo.dup
|
|
519
|
+
ammo: @player_state.ammo.dup,
|
|
520
|
+
attack_finished: @attack_finished
|
|
230
521
|
},
|
|
231
522
|
brush_entities: brush_entity_summaries,
|
|
232
523
|
particles: @particles ? @particles.particle_count : 0
|
|
@@ -249,7 +540,7 @@ module Quake
|
|
|
249
540
|
|
|
250
541
|
def brush_entity_summaries
|
|
251
542
|
return [] unless @brush_game
|
|
252
|
-
@entities.select
|
|
543
|
+
@entities.select { |ent| ent.brush_entity? && !ent.removed? }.map do |e|
|
|
253
544
|
{
|
|
254
545
|
classname: e.classname,
|
|
255
546
|
targetname: e.targetname,
|
|
@@ -259,11 +550,124 @@ module Quake
|
|
|
259
550
|
end
|
|
260
551
|
end
|
|
261
552
|
|
|
553
|
+
def viewmodel_visible?
|
|
554
|
+
@player_state&.alive? && !@player_state.powerup_item_active?(:ring, game_time: @game_time)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def render_viewmodel
|
|
558
|
+
return unless @viewmodel
|
|
559
|
+
|
|
560
|
+
camera = viewmodel_camera
|
|
561
|
+
ambient_light, shade_light = alias_entity_lighting(camera.position)
|
|
562
|
+
@viewmodel.render(
|
|
563
|
+
camera,
|
|
564
|
+
@window.aspect_ratio,
|
|
565
|
+
visible: viewmodel_visible?,
|
|
566
|
+
ambient_light: ambient_light,
|
|
567
|
+
shade_light: shade_light
|
|
568
|
+
)
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
ENTITY_HULL_MINS = Math::Vec3.new(-16.0, -16.0, -24.0)
|
|
572
|
+
ENTITY_HULL_MAXS = Math::Vec3.new(16.0, 16.0, 32.0)
|
|
573
|
+
GRAVITY = 800.0
|
|
574
|
+
STOP_EPSILON = 0.1
|
|
575
|
+
MOVETYPE_NONE = 0
|
|
576
|
+
MOVETYPE_WALK = 3
|
|
577
|
+
MOVETYPE_TOSS = 6
|
|
578
|
+
MOVETYPE_NOCLIP = 8
|
|
579
|
+
MOVETYPE_BOUNCE = 10
|
|
580
|
+
MOVETYPE_PUSH = 7
|
|
581
|
+
SOLID_NOT = 0
|
|
582
|
+
SOLID_BBOX = 2
|
|
583
|
+
SOLID_SLIDEBOX = 3
|
|
584
|
+
DAMAGE_AIM = 2
|
|
585
|
+
FL_ONGROUND = 512
|
|
586
|
+
GRENADE_FUSE = 2.5
|
|
587
|
+
ROCKET_LIFETIME = 5.0
|
|
588
|
+
DEFAULT_VIEWSIZE = 110 # status bar only; 100 adds the inventory bar
|
|
589
|
+
SPIKE_LIFETIME = 6.0
|
|
590
|
+
LASER_LIFETIME = 5.0
|
|
591
|
+
EF_ROCKET = 1
|
|
592
|
+
EF_GRENADE = 2
|
|
593
|
+
EF_GIB = 4
|
|
594
|
+
EF_ROTATE = 8
|
|
595
|
+
EF_TRACER = 16
|
|
596
|
+
EF_ZOMGIB = 32
|
|
597
|
+
EF_TRACER2 = 64
|
|
598
|
+
EF_TRACER3 = 128
|
|
599
|
+
ENTITY_EFFECT_BRIGHTFIELD = 1
|
|
600
|
+
ENTITY_EFFECT_MUZZLEFLASH = 2
|
|
601
|
+
ENTITY_EFFECT_BRIGHTLIGHT = 4
|
|
602
|
+
ENTITY_EFFECT_DIMLIGHT = 8
|
|
603
|
+
PLAYER_WEAPON_ITEM_BITS = {
|
|
604
|
+
shotgun: 1,
|
|
605
|
+
super_shotgun: 2,
|
|
606
|
+
nailgun: 4,
|
|
607
|
+
super_nailgun: 8,
|
|
608
|
+
grenade_launcher: 16,
|
|
609
|
+
rocket_launcher: 32,
|
|
610
|
+
lightning_gun: 64,
|
|
611
|
+
axe: 4096
|
|
612
|
+
}.freeze
|
|
613
|
+
PLAYER_WEAPON_NETNAMES = {
|
|
614
|
+
axe: "Axe",
|
|
615
|
+
shotgun: "Shotgun",
|
|
616
|
+
super_shotgun: "Double-barrelled Shotgun",
|
|
617
|
+
nailgun: "Nailgun",
|
|
618
|
+
super_nailgun: "Super Nailgun",
|
|
619
|
+
grenade_launcher: "Grenade Launcher",
|
|
620
|
+
rocket_launcher: "Rocket Launcher",
|
|
621
|
+
lightning_gun: "Thunderbolt"
|
|
622
|
+
}.freeze
|
|
623
|
+
TEMP_ENTITY_MODELS = %w[
|
|
624
|
+
progs/bolt.mdl progs/bolt2.mdl progs/bolt3.mdl
|
|
625
|
+
].freeze
|
|
626
|
+
MAX_BEAMS = 24
|
|
627
|
+
VIEW_ENTITY = 1
|
|
628
|
+
VIEW_ORIGIN_NUDGE = 1.0 / 32.0
|
|
629
|
+
VIEW_ORIGIN_NUDGE_VECTOR = Math::Vec3.new(
|
|
630
|
+
VIEW_ORIGIN_NUDGE,
|
|
631
|
+
VIEW_ORIGIN_NUDGE,
|
|
632
|
+
VIEW_ORIGIN_NUDGE
|
|
633
|
+
)
|
|
634
|
+
VIEW_BOUND_XY = 14.0
|
|
635
|
+
VIEW_BOUND_Z_MIN = -22.0
|
|
636
|
+
VIEW_BOUND_Z_MAX = 30.0
|
|
637
|
+
STAIR_SMOOTH_SPEED = 80.0
|
|
638
|
+
STAIR_SMOOTH_MAX = 12.0
|
|
639
|
+
VIEW_IDLE_YAW_CYCLE = 2.0
|
|
640
|
+
VIEW_IDLE_ROLL_CYCLE = 0.5
|
|
641
|
+
VIEW_IDLE_PITCH_CYCLE = 1.0
|
|
642
|
+
VIEW_IDLE_YAW_LEVEL = 0.3
|
|
643
|
+
VIEW_IDLE_ROLL_LEVEL = 0.1
|
|
644
|
+
VIEW_IDLE_PITCH_LEVEL = 0.3
|
|
645
|
+
VIEW_CENTER_MOVE = 0.15
|
|
646
|
+
VIEW_CENTER_SPEED = 500.0
|
|
647
|
+
WEAPON_PUNCH_DECAY = 10.0
|
|
648
|
+
WEAPON_PUNCH_ANGLES = {
|
|
649
|
+
small: Math::Vec3.new(-2.0, 0.0, 0.0),
|
|
650
|
+
big: Math::Vec3.new(-4.0, 0.0, 0.0)
|
|
651
|
+
}.freeze
|
|
652
|
+
Projectile = Data.define(
|
|
653
|
+
:kind, :position, :velocity, :damage, :damage_variance, :radius_damage, :explode_at,
|
|
654
|
+
:model, :movetype, :solid, :effects, :angles, :avelocity, :voided, :owner
|
|
655
|
+
) do
|
|
656
|
+
def classname
|
|
657
|
+
kind == :fireball ? "fireball" : kind.to_s
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
DynamicLight = Data.define(:key, :origin, :radius, :minlight, :die)
|
|
661
|
+
TemporaryDynamicLight = Data.define(:origin, :radius, :minlight, :start_time, :die, :decay)
|
|
662
|
+
TemporaryBeam = Data.define(:entity, :model, :end_time, :start, :finish)
|
|
663
|
+
TemporaryBeamSegment = Data.define(:model, :origin, :pitch, :yaw, :roll)
|
|
664
|
+
WeaponSound = Data.define(:sound)
|
|
665
|
+
|
|
262
666
|
def build_renderers
|
|
263
667
|
@tex_manager = Renderer::GLTextureManager.new(@level, @palette)
|
|
264
668
|
@tex_manager.upload_all
|
|
265
669
|
|
|
266
|
-
@lightmap = Renderer::GLLightmap.new(@level, @palette)
|
|
670
|
+
@lightmap = Renderer::GLLightmap.new(@level, @palette, lightstyles: current_lightstyles)
|
|
267
671
|
@lightmap.build_all
|
|
268
672
|
|
|
269
673
|
@sky = Renderer::GLSky.new(@level, @palette, @tex_manager)
|
|
@@ -279,22 +683,3937 @@ module Quake
|
|
|
279
683
|
@mdl_renderers = {}
|
|
280
684
|
@mdl_cache.each do |path, mdl|
|
|
281
685
|
next unless mdl
|
|
282
|
-
@mdl_renderers[path] = Renderer::GLAliasModel.new(mdl, @palette)
|
|
686
|
+
@mdl_renderers[path] = Renderer::GLAliasModel.new(mdl, @palette, model_name: path)
|
|
283
687
|
end
|
|
284
688
|
|
|
285
689
|
@viewmodel = Renderer::GLViewmodel.new(@pak, @palette)
|
|
286
690
|
@viewmodel.set_weapon(@player_state.current_weapon_model)
|
|
287
691
|
|
|
288
|
-
@hud = Renderer::GLHud.new(@wad, @palette, @window.width, @window.height) if @wad
|
|
692
|
+
@hud = Renderer::GLHud.new(@wad, @palette, @window.width, @window.height, pak: @pak) if @wad
|
|
289
693
|
@particles = Renderer::GLParticles.new(@palette)
|
|
694
|
+
@view_blend = Renderer::GLViewBlend.new
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
def load_mdl_model(model_path)
|
|
698
|
+
return unless model_path&.end_with?(".mdl")
|
|
699
|
+
return if @mdl_cache.key?(model_path)
|
|
700
|
+
|
|
701
|
+
begin
|
|
702
|
+
mdl_data = @pak.read(model_path)
|
|
703
|
+
@mdl_cache[model_path] = Mdl::Reader.new(mdl_data).parse
|
|
704
|
+
rescue => e
|
|
705
|
+
warn "Failed to load #{model_path}: #{e.message}"
|
|
706
|
+
@mdl_cache[model_path] = nil
|
|
707
|
+
end
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def apply_start_map_parms
|
|
711
|
+
return unless @current_map == "maps/start.bsp"
|
|
712
|
+
return if @player_state.serverflags.zero?
|
|
713
|
+
|
|
714
|
+
@player_state.reset_to_new_parms
|
|
290
715
|
end
|
|
291
716
|
|
|
292
717
|
def handle_trigger(trigger)
|
|
293
718
|
case trigger.classname
|
|
294
719
|
when "trigger_changelevel"
|
|
295
|
-
|
|
720
|
+
if samelevel_blocks_changelevel?
|
|
721
|
+
damage_player(50_000, inflictor: trigger)
|
|
722
|
+
return
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
@brush_game.use_targets(trigger, activator: @player_state)
|
|
726
|
+
queue_changelevel(trigger)
|
|
727
|
+
when "trigger_teleport"
|
|
728
|
+
return if trigger.targetname && (trigger.think_time.zero? || trigger.think_time < @game_time)
|
|
729
|
+
return unless @player_state.alive?
|
|
730
|
+
|
|
731
|
+
@brush_game.use_targets(trigger, activator: @player_state)
|
|
732
|
+
teleport_trigger(trigger)
|
|
733
|
+
when "trigger_hurt"
|
|
734
|
+
hurt_trigger(trigger)
|
|
735
|
+
when "trigger_push"
|
|
736
|
+
push_trigger(trigger)
|
|
737
|
+
else
|
|
738
|
+
@brush_game.touch_trigger(trigger, activator: @player_state)
|
|
739
|
+
end
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
def queue_changelevel(trigger)
|
|
743
|
+
map = trigger["map"]
|
|
744
|
+
raise "chagnelevel trigger doesn't have map" if map.nil? || map.empty?
|
|
745
|
+
|
|
746
|
+
trigger.instance_variable_set(:@touch_disabled, true)
|
|
747
|
+
trigger.instance_variable_set(:@think, :execute_changelevel)
|
|
748
|
+
trigger.think_time = @game_time.to_f + 0.1
|
|
749
|
+
@pending_changelevel = normalize_map_name(map)
|
|
750
|
+
@pending_changelevel_time = [@pending_changelevel_time || trigger.think_time, trigger.think_time].min
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
def samelevel_blocks_changelevel?
|
|
754
|
+
@samelevel.to_i == 2 || (@samelevel.to_i == 3 && current_quake_mapname != "start")
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
def next_level
|
|
758
|
+
return if @pending_changelevel
|
|
759
|
+
|
|
760
|
+
mapname = current_quake_mapname
|
|
761
|
+
trigger = if @samelevel.to_i == 1
|
|
762
|
+
Entity.new("map" => mapname)
|
|
763
|
+
elsif mapname == "start"
|
|
764
|
+
Entity.new("map" => start_map_next_level)
|
|
765
|
+
else
|
|
766
|
+
@entities&.find { |ent| ent.classname == "trigger_changelevel" } ||
|
|
767
|
+
Entity.new("map" => mapname)
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
queue_changelevel(trigger)
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
def check_rules
|
|
774
|
+
next_level if timelimit.to_f.nonzero? && @game_time.to_f >= timelimit.to_f
|
|
775
|
+
next_level if fraglimit.to_i.nonzero? && @player_state.frags.to_i >= fraglimit.to_i
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
def current_quake_mapname
|
|
779
|
+
File.basename(@current_map.to_s, ".bsp")
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
def start_map_next_level
|
|
783
|
+
return "e1m1" unless @registered
|
|
784
|
+
|
|
785
|
+
if (@player_state.serverflags & 1).zero?
|
|
786
|
+
set_serverflags(@player_state.serverflags | 1)
|
|
787
|
+
"e1m1"
|
|
788
|
+
elsif (@player_state.serverflags & 2).zero?
|
|
789
|
+
set_serverflags(@player_state.serverflags | 2)
|
|
790
|
+
"e2m1"
|
|
791
|
+
elsif (@player_state.serverflags & 4).zero?
|
|
792
|
+
set_serverflags(@player_state.serverflags | 4)
|
|
793
|
+
"e3m1"
|
|
794
|
+
elsif (@player_state.serverflags & 8).zero?
|
|
795
|
+
set_serverflags(@player_state.serverflags - 7)
|
|
796
|
+
"e4m1"
|
|
797
|
+
else
|
|
798
|
+
"start"
|
|
799
|
+
end
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
def set_serverflags(flags)
|
|
803
|
+
@player_state.instance_variable_set(:@serverflags, flags.to_i)
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
def process_item_pickup_targets(event)
|
|
807
|
+
entity = event[:entity]
|
|
808
|
+
return unless entity
|
|
809
|
+
return if event[:use_targets] == false
|
|
810
|
+
return if event[:type] == :backpack || entity.classname == "item_backpack"
|
|
811
|
+
|
|
812
|
+
@brush_game&.use_targets(entity, activator: @player_state)
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
def process_item_pickup_feedback(event)
|
|
816
|
+
sprint(event[:message]) if event[:message]
|
|
817
|
+
@sound_events&.on_pickup(event)
|
|
818
|
+
trigger_bonus_flash
|
|
819
|
+
@particles&.pickup_effect(event[:entity].position)
|
|
820
|
+
@viewmodel&.set_weapon(@player_state.current_weapon_model)
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
def normalize_map_name(map)
|
|
824
|
+
map = map.to_s
|
|
825
|
+
map = "maps/#{map}" unless map.include?("/")
|
|
826
|
+
map.end_with?(".bsp") ? map : "#{map}.bsp"
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
def run_pending_changelevel
|
|
830
|
+
@intermission_next_map = changelevel_destination_map(@pending_changelevel)
|
|
831
|
+
@pending_changelevel = nil
|
|
832
|
+
@pending_changelevel_time = nil
|
|
833
|
+
execute_changelevel
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
def execute_changelevel
|
|
837
|
+
@intermission_running = true
|
|
838
|
+
@intermission_start_time = @game_time.to_f
|
|
839
|
+
@intermission_exittime = @game_time.to_f + 5.0
|
|
840
|
+
@intermission_spot = find_intermission_spot
|
|
841
|
+
@intermission_origin = @intermission_spot.position
|
|
842
|
+
@intermission_angles = vec3_property(@intermission_spot, "mangle")
|
|
843
|
+
freeze_intermission_player(@player) if @player
|
|
844
|
+
(@entities || []).each { |ent| freeze_intermission_player(ent) if ent.classname == "player" }
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
def freeze_intermission_player(player)
|
|
848
|
+
player.instance_variable_set(:@takedamage, 0)
|
|
849
|
+
player.instance_variable_set(:@solid, SOLID_NOT)
|
|
850
|
+
player.instance_variable_set(:@movetype, MOVETYPE_NONE)
|
|
851
|
+
if player.respond_to?(:model_index=)
|
|
852
|
+
player.model_index = 0
|
|
853
|
+
else
|
|
854
|
+
player.instance_variable_set(:@modelindex, 0)
|
|
855
|
+
end
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
def intermission_think
|
|
859
|
+
return if @game_time.to_f < @intermission_exittime.to_f
|
|
860
|
+
return unless intermission_exit_button_down?
|
|
861
|
+
|
|
862
|
+
complete_intermission_changelevel
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
def intermission_exit_button_down?
|
|
866
|
+
@attack_pressed || (@keys || {})[SDL::SCANCODE_SPACE]
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
def complete_intermission_changelevel
|
|
870
|
+
next_map = @intermission_next_map
|
|
871
|
+
@intermission_running = false
|
|
872
|
+
@intermission_next_map = nil
|
|
873
|
+
@intermission_spot = nil
|
|
874
|
+
@intermission_origin = nil
|
|
875
|
+
@intermission_angles = nil
|
|
876
|
+
|
|
877
|
+
# QuakeC changelevel_touch saves SetChangeParms before Host_Changelevel
|
|
878
|
+
# spawns the next server. PlayerState remains persistent after applying
|
|
879
|
+
# those transition rules, while player/camera reset to the new start.
|
|
880
|
+
@player_state.apply_changelevel_parms
|
|
881
|
+
load_map(next_map)
|
|
882
|
+
end
|
|
883
|
+
|
|
884
|
+
def changelevel_destination_map(next_map)
|
|
885
|
+
if @samelevel.to_i == 1
|
|
886
|
+
normalize_map_name(current_quake_mapname)
|
|
887
|
+
else
|
|
888
|
+
override = world_info_value(current_quake_mapname)
|
|
889
|
+
normalize_map_name(override.nil? || override.empty? ? next_map : override)
|
|
890
|
+
end
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
def teleport_trigger(trigger)
|
|
894
|
+
target = trigger.target
|
|
895
|
+
raise "trigger_teleport doesn't have target" if target.nil? || target.empty?
|
|
896
|
+
|
|
897
|
+
dest = @target_map[target]&.find { |ent| ent.classname == "info_teleport_destination" } ||
|
|
898
|
+
@target_map[target]&.first
|
|
899
|
+
raise "couldn't find target" unless dest
|
|
900
|
+
|
|
901
|
+
setup_teleport_destination(dest) if dest.classname == "info_teleport_destination"
|
|
902
|
+
old_pos = @player.position
|
|
903
|
+
dest_pos = dest.position
|
|
904
|
+
mangle = dest.instance_variable_get(:@mangle) || dest.angles
|
|
905
|
+
forward = angle_forward(mangle)
|
|
906
|
+
spawn_teleport_fog(old_pos)
|
|
907
|
+
spawn_teleport_fog(dest_pos + forward * 32.0)
|
|
908
|
+
apply_telefrag_damage(dest_pos)
|
|
909
|
+
if @player_state.health <= 0
|
|
910
|
+
velocity = @player.velocity
|
|
911
|
+
@player.position = dest_pos
|
|
912
|
+
@player.velocity = forward * velocity.x + forward * velocity.y
|
|
913
|
+
return
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
teleport(dest_pos.x, dest_pos.y, dest_pos.z, yaw: mangle.y, pitch: 0.0)
|
|
917
|
+
|
|
918
|
+
forward = @player.forward_flat
|
|
919
|
+
@player.velocity = forward * 300.0
|
|
920
|
+
@player.instance_variable_set(:@fixangle, true)
|
|
921
|
+
@player.instance_variable_set(:@teleport_time, @game_time.to_f + 0.7)
|
|
922
|
+
@player_state.flags &= ~FL_ONGROUND
|
|
923
|
+
@player.on_ground = false
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
def spawn_teleport_fog(origin)
|
|
927
|
+
@particles&.teleport_splash(origin)
|
|
928
|
+
return unless @entities
|
|
929
|
+
|
|
930
|
+
fog = Entity.new("classname" => "teleport_fog", "origin" => vec3_string(origin))
|
|
931
|
+
fog.think_time = @game_time.to_f + 0.2
|
|
932
|
+
fog.instance_variable_set(:@think, :play_teleport)
|
|
933
|
+
@entities << fog
|
|
934
|
+
fog
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
def apply_telefrag_damage(origin)
|
|
938
|
+
teledeath = teledeath_entity(origin)
|
|
939
|
+
mins = origin + ENTITY_HULL_MINS - Math::Vec3.new(1.0, 1.0, 1.0)
|
|
940
|
+
maxs = origin + ENTITY_HULL_MAXS + Math::Vec3.new(1.0, 1.0, 1.0)
|
|
941
|
+
|
|
942
|
+
@entities.each do |ent|
|
|
943
|
+
next unless ent.damageable?
|
|
944
|
+
next if ent.health.to_f.zero?
|
|
945
|
+
|
|
946
|
+
ent_mins, ent_maxs = damageable_bounds(ent)
|
|
947
|
+
next unless ent_mins && ent_maxs
|
|
948
|
+
next unless aabb_intersect?(mins, maxs, ent_mins, ent_maxs)
|
|
949
|
+
|
|
950
|
+
if ent.classname == "player" && ent.invincible_finished.to_f > @game_time.to_f
|
|
951
|
+
if player_invincible_finished > @game_time.to_f
|
|
952
|
+
teledeath.classname = "teledeath3"
|
|
953
|
+
ent.invincible_finished = 0.0
|
|
954
|
+
clear_player_invincibility
|
|
955
|
+
damage_entity(ent, 50_000.0, attacker: teledeath, inflictor: teledeath)
|
|
956
|
+
teledeath.instance_variable_set(:@owner, ent)
|
|
957
|
+
@player_state.deathtype = "teledeath3"
|
|
958
|
+
damage_player(50_000.0, inflictor: teledeath)
|
|
959
|
+
next
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
teledeath.classname = "teledeath2"
|
|
963
|
+
@player_state.deathtype = "teledeath2"
|
|
964
|
+
damage_player(50_000.0, inflictor: teledeath)
|
|
965
|
+
next
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
damage_entity(ent, 50_000.0, attacker: teledeath, inflictor: teledeath)
|
|
969
|
+
end
|
|
970
|
+
end
|
|
971
|
+
|
|
972
|
+
def player_invincible_finished
|
|
973
|
+
@player_state&.powerup_finished&.fetch(:pentagram, 0.0).to_f
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
def clear_player_invincibility
|
|
977
|
+
@player_state&.powerup_finished&.delete(:pentagram)
|
|
978
|
+
@player_state&.powerup_warning_time&.delete(:pentagram)
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
def teledeath_entity(origin)
|
|
982
|
+
Entity.new("classname" => "teledeath", "origin" => "#{origin.x} #{origin.y} #{origin.z}").tap do |ent|
|
|
983
|
+
ent.instance_variable_set(:@owner, @player_state)
|
|
984
|
+
ent.instance_variable_set(:@movetype, MOVETYPE_NONE)
|
|
985
|
+
ent.instance_variable_set(:@solid, 1)
|
|
986
|
+
end
|
|
987
|
+
end
|
|
988
|
+
|
|
989
|
+
def hurt_trigger(trigger)
|
|
990
|
+
return unless player_takedamage?
|
|
991
|
+
|
|
992
|
+
damage_player(trigger_hurt_damage(trigger), pain_sound: true, inflictor: trigger)
|
|
993
|
+
trigger.instance_variable_set(:@touch_disabled, true)
|
|
994
|
+
trigger.instance_variable_set(:@solid, 0)
|
|
995
|
+
trigger.instance_variable_set(:@think, :hurt_on)
|
|
996
|
+
# hurt_touch re-arms 1s later; update_trigger_cooldown counts
|
|
997
|
+
# think_time down as a duration, so store the delay, not a time
|
|
998
|
+
trigger.think_time = 1.0
|
|
999
|
+
end
|
|
1000
|
+
|
|
1001
|
+
def player_takedamage?
|
|
1002
|
+
return @player_state&.alive? unless @player
|
|
1003
|
+
|
|
1004
|
+
return @player_state&.alive? unless @player.instance_variable_defined?(:@takedamage)
|
|
1005
|
+
|
|
1006
|
+
@player.instance_variable_get(:@takedamage).to_i != 0
|
|
1007
|
+
end
|
|
1008
|
+
|
|
1009
|
+
def trigger_hurt_damage(trigger)
|
|
1010
|
+
dmg = trigger["dmg"].to_f
|
|
1011
|
+
dmg.zero? ? 5.0 : dmg
|
|
1012
|
+
end
|
|
1013
|
+
|
|
1014
|
+
def push_trigger(trigger)
|
|
1015
|
+
if @player_state.alive?
|
|
1016
|
+
@player.velocity = trigger_movedir(trigger) * trigger.speed * 10.0
|
|
1017
|
+
@player.on_ground = false
|
|
1018
|
+
if @player.instance_variable_get(:@fly_sound).to_f < @game_time
|
|
1019
|
+
@player.instance_variable_set(:@fly_sound, @game_time + 1.5)
|
|
1020
|
+
@sound_events&.on_trigger_push_player
|
|
1021
|
+
end
|
|
1022
|
+
end
|
|
1023
|
+
trigger.instance_variable_set(:@removed, true) if (trigger.spawnflags & 1) != 0
|
|
1024
|
+
end
|
|
1025
|
+
|
|
1026
|
+
# trigger_push_touch pushes grenades too: velocity = movedir*speed*10
|
|
1027
|
+
def grenade_push_trigger_velocity(position)
|
|
1028
|
+
(@entities || []).each do |trigger|
|
|
1029
|
+
next unless trigger.classname == "trigger_push"
|
|
1030
|
+
next if trigger.removed?
|
|
1031
|
+
next unless trigger.model_index
|
|
1032
|
+
|
|
1033
|
+
model = @level&.models&.[](trigger.model_index)
|
|
1034
|
+
next unless model
|
|
1035
|
+
next unless position.x >= trigger.position.x + model.mins.x &&
|
|
1036
|
+
position.x <= trigger.position.x + model.maxs.x &&
|
|
1037
|
+
position.y >= trigger.position.y + model.mins.y &&
|
|
1038
|
+
position.y <= trigger.position.y + model.maxs.y &&
|
|
1039
|
+
position.z >= trigger.position.z + model.mins.z &&
|
|
1040
|
+
position.z <= trigger.position.z + model.maxs.z
|
|
1041
|
+
|
|
1042
|
+
velocity = trigger_movedir(trigger) * trigger.speed * 10.0
|
|
1043
|
+
trigger.instance_variable_set(:@removed, true) if (trigger.spawnflags & 1) != 0
|
|
1044
|
+
return velocity
|
|
1045
|
+
end
|
|
1046
|
+
nil
|
|
1047
|
+
end
|
|
1048
|
+
|
|
1049
|
+
def trigger_movedir(trigger)
|
|
1050
|
+
return trigger.move_dir if trigger.move_dir != Math::Vec3::ORIGIN
|
|
1051
|
+
return trigger.forward_vector if trigger["angles"] && trigger["angles"].split.map(&:to_f) != [0.0, 0.0, 0.0]
|
|
1052
|
+
return trigger.forward_vector if trigger["angle"] && trigger["angle"].to_f != 0.0
|
|
1053
|
+
|
|
1054
|
+
Math::Vec3::ORIGIN
|
|
1055
|
+
end
|
|
1056
|
+
|
|
1057
|
+
def apply_environment_damage(dt = @view_frame_dt.to_f)
|
|
1058
|
+
return if @player&.noclip
|
|
1059
|
+
return if @player_state&.health&.negative?
|
|
1060
|
+
|
|
1061
|
+
apply_drowning
|
|
1062
|
+
if @player.water_level.zero?
|
|
1063
|
+
update_water_transition_sounds
|
|
1064
|
+
return
|
|
1065
|
+
end
|
|
1066
|
+
|
|
1067
|
+
if @game_time > (@environment_damage_time || 0.0)
|
|
1068
|
+
case @player.water_type
|
|
1069
|
+
when Physics::CONTENTS_LAVA
|
|
1070
|
+
delay = biosuit_slows_lava_damage? ? 1.0 : 0.2
|
|
1071
|
+
@environment_damage_time = @game_time + delay
|
|
1072
|
+
damage_player(10.0 * @player.water_level)
|
|
1073
|
+
when Physics::CONTENTS_SLIME
|
|
1074
|
+
unless biosuit_blocks_slime_damage?
|
|
1075
|
+
@environment_damage_time = @game_time + 1.0
|
|
1076
|
+
damage_player(4.0 * @player.water_level)
|
|
1077
|
+
end
|
|
1078
|
+
end
|
|
1079
|
+
end
|
|
1080
|
+
|
|
1081
|
+
update_water_transition_sounds
|
|
1082
|
+
apply_water_velocity_drag(dt)
|
|
1083
|
+
end
|
|
1084
|
+
|
|
1085
|
+
def update_water_transition_sounds
|
|
1086
|
+
if @player.water_level.zero?
|
|
1087
|
+
if @player_in_water
|
|
1088
|
+
@sound_events&.on_player(:water_exit)
|
|
1089
|
+
@player_in_water = false
|
|
1090
|
+
end
|
|
1091
|
+
return
|
|
1092
|
+
end
|
|
1093
|
+
|
|
1094
|
+
return if @player_in_water
|
|
1095
|
+
|
|
1096
|
+
@player_in_water = true
|
|
1097
|
+
case @player.water_type
|
|
1098
|
+
when Physics::CONTENTS_LAVA
|
|
1099
|
+
@sound_events&.on_player(:lava_enter)
|
|
1100
|
+
when Physics::CONTENTS_SLIME
|
|
1101
|
+
@sound_events&.on_player(:slime_enter)
|
|
1102
|
+
when Physics::CONTENTS_WATER
|
|
1103
|
+
@sound_events&.on_player(:water_enter)
|
|
1104
|
+
end
|
|
1105
|
+
@environment_damage_time = 0.0
|
|
1106
|
+
end
|
|
1107
|
+
|
|
1108
|
+
def play_jump_sounds
|
|
1109
|
+
return unless @keys[SDL::SCANCODE_SPACE]
|
|
1110
|
+
return if @player_state && (@player_state.flags & PlayerState::FL_WATERJUMP) != 0
|
|
1111
|
+
|
|
1112
|
+
if @player.water_level >= 2
|
|
1113
|
+
return unless (@swim_sound_finished || 0.0) < @game_time
|
|
1114
|
+
|
|
1115
|
+
@swim_sound_finished = @game_time + 1.0
|
|
1116
|
+
@sound_events&.on_player(rand < 0.5 ? :swim1 : :swim2)
|
|
1117
|
+
return
|
|
1118
|
+
end
|
|
1119
|
+
|
|
1120
|
+
return unless @player.on_ground
|
|
1121
|
+
return if (@player_state.flags & PlayerState::FL_JUMPRELEASED).zero?
|
|
1122
|
+
return if @player.jump_held
|
|
1123
|
+
|
|
1124
|
+
@sound_events&.on_player(:jump)
|
|
1125
|
+
end
|
|
1126
|
+
|
|
1127
|
+
def apply_fall_damage
|
|
1128
|
+
return unless @player_state.alive?
|
|
1129
|
+
|
|
1130
|
+
if @jump_flag < -300.0 && @player.on_ground
|
|
1131
|
+
if @player.water_type == Physics::CONTENTS_WATER
|
|
1132
|
+
@sound_events&.on_player(:water_land)
|
|
1133
|
+
elsif @jump_flag < -650.0
|
|
1134
|
+
@player_state.deathtype = "falling"
|
|
1135
|
+
damage_player(5)
|
|
1136
|
+
@sound_events&.on_player(:land_hurt)
|
|
1137
|
+
else
|
|
1138
|
+
@sound_events&.on_player(:land)
|
|
1139
|
+
end
|
|
1140
|
+
end
|
|
1141
|
+
|
|
1142
|
+
@jump_flag = @player.velocity.z
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
def apply_drowning
|
|
1146
|
+
if @player.water_level != 3
|
|
1147
|
+
if @air_finished < @game_time
|
|
1148
|
+
@sound_events&.on_player(:gasp_deep)
|
|
1149
|
+
elsif @air_finished < @game_time + 9.0
|
|
1150
|
+
@sound_events&.on_player(:gasp)
|
|
1151
|
+
end
|
|
1152
|
+
@air_finished = @game_time + 12.0
|
|
1153
|
+
@drown_damage = 2.0
|
|
1154
|
+
return
|
|
1155
|
+
end
|
|
1156
|
+
|
|
1157
|
+
return unless @air_finished < @game_time
|
|
1158
|
+
return unless @drown_pain_finished < @game_time
|
|
1159
|
+
|
|
1160
|
+
@drown_damage += 2.0
|
|
1161
|
+
@drown_damage = 10.0 if @drown_damage > 15.0
|
|
1162
|
+
result = damage_player(@drown_damage)
|
|
1163
|
+
@sound_events&.on_player(rand > 0.5 ? :drown1 : :drown2) if result[:take].positive? && @player_state.alive?
|
|
1164
|
+
@drown_pain_finished = @game_time + 1.0
|
|
1165
|
+
end
|
|
1166
|
+
|
|
1167
|
+
def biosuit_blocks_slime_damage?
|
|
1168
|
+
finished = @player_state.powerup_finished[:biosuit]
|
|
1169
|
+
finished && finished >= @game_time
|
|
1170
|
+
end
|
|
1171
|
+
|
|
1172
|
+
def biosuit_slows_lava_damage?
|
|
1173
|
+
finished = @player_state.powerup_finished[:biosuit]
|
|
1174
|
+
finished && finished > @game_time
|
|
1175
|
+
end
|
|
1176
|
+
|
|
1177
|
+
def apply_water_velocity_drag(dt)
|
|
1178
|
+
return if dt.to_f.zero?
|
|
1179
|
+
return if (@player_state.flags & PlayerState::FL_WATERJUMP) != 0
|
|
1180
|
+
|
|
1181
|
+
scale = 1.0 - 0.8 * @player.water_level.to_f * dt.to_f
|
|
1182
|
+
@player.velocity = @player.velocity * scale
|
|
1183
|
+
end
|
|
1184
|
+
|
|
1185
|
+
def damage_player(amount, pain_sound: false, inflictor: nil, attacker: nil, apply_momentum: true)
|
|
1186
|
+
if @player_state.invulnerable_to_damage?(game_time: @game_time) &&
|
|
1187
|
+
(@invincible_sound_finished || 0.0) < @game_time
|
|
1188
|
+
@invincible_sound_finished = @game_time + 2.0
|
|
1189
|
+
@sound_events&.on_player(:pentagram_protect)
|
|
1190
|
+
end
|
|
1191
|
+
|
|
1192
|
+
was_alive = @player_state.alive?
|
|
1193
|
+
apply_player_direct_damage_momentum(amount, inflictor) if apply_momentum
|
|
1194
|
+
result = @player_state.take_damage(amount, game_time: @game_time)
|
|
1195
|
+
result = result.merge(display_take: (amount.to_f - result[:save].to_f).ceil)
|
|
1196
|
+
trigger_damage_flash(result)
|
|
1197
|
+
trigger_damage_kick(result, inflictor)
|
|
1198
|
+
if was_alive && !@player_state.alive?
|
|
1199
|
+
apply_player_obituary_frags(attacker || inflictor)
|
|
1200
|
+
drop_player_powerups
|
|
1201
|
+
drop_player_backpack
|
|
1202
|
+
apply_player_death_state(inflictor: inflictor)
|
|
1203
|
+
@player_state.clear_powerups_on_death
|
|
1204
|
+
play_death_sound(attacker: attacker || inflictor)
|
|
1205
|
+
select_player_death_animation
|
|
1206
|
+
return result
|
|
1207
|
+
end
|
|
1208
|
+
|
|
1209
|
+
if result[:take].positive?
|
|
1210
|
+
select_player_pain_animation
|
|
1211
|
+
if pain_sound
|
|
1212
|
+
play_player_pain_sound(attacker: attacker || inflictor)
|
|
1213
|
+
else
|
|
1214
|
+
play_environment_pain_sound
|
|
1215
|
+
end
|
|
1216
|
+
end
|
|
1217
|
+
result
|
|
1218
|
+
end
|
|
1219
|
+
|
|
1220
|
+
def play_player_pain_sound(attacker: nil)
|
|
1221
|
+
if attacker_classname(attacker) == "teledeath"
|
|
1222
|
+
@sound_events&.on_player(:teledeath)
|
|
1223
|
+
return
|
|
1224
|
+
end
|
|
1225
|
+
|
|
1226
|
+
if @player&.water_type == Physics::CONTENTS_WATER && @player.water_level == 3
|
|
1227
|
+
spawn_death_bubbles(1)
|
|
1228
|
+
@sound_events&.on_player(rand > 0.5 ? :drown1 : :drown2)
|
|
1229
|
+
return
|
|
1230
|
+
end
|
|
1231
|
+
|
|
1232
|
+
case @player&.water_type
|
|
1233
|
+
when Physics::CONTENTS_LAVA, Physics::CONTENTS_SLIME
|
|
1234
|
+
@sound_events&.on_player(rand > 0.5 ? :burn1 : :burn2)
|
|
1235
|
+
else
|
|
1236
|
+
play_generic_pain_sound
|
|
1237
|
+
end
|
|
1238
|
+
end
|
|
1239
|
+
|
|
1240
|
+
def play_environment_pain_sound
|
|
1241
|
+
case @player&.water_type
|
|
1242
|
+
when Physics::CONTENTS_LAVA, Physics::CONTENTS_SLIME
|
|
1243
|
+
@sound_events&.on_player(rand > 0.5 ? :burn1 : :burn2)
|
|
1244
|
+
end
|
|
1245
|
+
end
|
|
1246
|
+
|
|
1247
|
+
def play_generic_pain_sound
|
|
1248
|
+
if (@pain_sound_finished || 0.0) > @game_time
|
|
1249
|
+
@player&.instance_variable_set(:@axhitme, 0)
|
|
1250
|
+
return
|
|
1251
|
+
end
|
|
1252
|
+
|
|
1253
|
+
@pain_sound_finished = @game_time + 0.5
|
|
1254
|
+
if @player&.instance_variable_get(:@axhitme).to_i == 1
|
|
1255
|
+
@player.instance_variable_set(:@axhitme, 0)
|
|
1256
|
+
@sound_events&.on_player(:axe_body_hit)
|
|
1257
|
+
return
|
|
1258
|
+
end
|
|
1259
|
+
|
|
1260
|
+
index = ((rand * 5.0) + 1.0).round
|
|
1261
|
+
index = [[index, 1].max, 6].min
|
|
1262
|
+
@sound_events&.on_player(:"pain#{index}")
|
|
1263
|
+
end
|
|
1264
|
+
|
|
1265
|
+
def play_death_sound(attacker: nil)
|
|
1266
|
+
if @player_state.health < -40
|
|
1267
|
+
if %w[teledeath teledeath2].include?(attacker_classname(attacker)) ||
|
|
1268
|
+
%w[teledeath teledeath2].include?(@player_state.deathtype)
|
|
1269
|
+
@sound_events&.on_player(:teledeath)
|
|
1270
|
+
return
|
|
1271
|
+
end
|
|
1272
|
+
|
|
1273
|
+
@sound_events&.on_player(rand < 0.5 ? :gib : :udeath)
|
|
1274
|
+
return
|
|
1275
|
+
end
|
|
1276
|
+
|
|
1277
|
+
if @player&.water_level == 3
|
|
1278
|
+
spawn_death_bubbles(5)
|
|
1279
|
+
@sound_events&.on_player(:water_death)
|
|
1280
|
+
return
|
|
1281
|
+
end
|
|
1282
|
+
|
|
1283
|
+
index = ((rand * 4.0) + 1.0).round
|
|
1284
|
+
index = [[index, 1].max, 5].min
|
|
1285
|
+
@sound_events&.on_player(:"death#{index}")
|
|
1286
|
+
end
|
|
1287
|
+
|
|
1288
|
+
def apply_player_obituary_frags(attacker)
|
|
1289
|
+
if deathmatch.to_i > 3 && @player_state.deathtype == "selfwater"
|
|
1290
|
+
@player_state.frags -= 1
|
|
1291
|
+
return
|
|
1292
|
+
end
|
|
1293
|
+
|
|
1294
|
+
if @player_state.deathtype == "squish"
|
|
1295
|
+
apply_player_squish_obituary_frags(attacker)
|
|
1296
|
+
return
|
|
1297
|
+
end
|
|
1298
|
+
|
|
1299
|
+
case attacker_classname(attacker)
|
|
1300
|
+
when "teledeath"
|
|
1301
|
+
telefrag_owner(attacker)&.frags += 1
|
|
1302
|
+
when "teledeath2", "teledeath3"
|
|
1303
|
+
@player_state.frags -= 1
|
|
1304
|
+
when "player"
|
|
1305
|
+
apply_player_attacker_obituary_frags(attacker)
|
|
1306
|
+
else
|
|
1307
|
+
@player_state.frags -= 1
|
|
1308
|
+
end
|
|
1309
|
+
end
|
|
1310
|
+
|
|
1311
|
+
def apply_player_squish_obituary_frags(attacker)
|
|
1312
|
+
if teamplay.to_i.nonzero? &&
|
|
1313
|
+
!quake_info_key(attacker, "team").empty? &&
|
|
1314
|
+
quake_info_key(attacker, "team") == quake_info_key(@player_state, "team") &&
|
|
1315
|
+
!attacker.equal?(@player_state)
|
|
1316
|
+
attacker.frags -= 1 if attacker.respond_to?(:frags=)
|
|
1317
|
+
elsif attacker_classname(attacker) == "player" && !attacker.equal?(@player_state)
|
|
1318
|
+
attacker.frags += 1 if attacker.respond_to?(:frags=)
|
|
1319
|
+
else
|
|
1320
|
+
@player_state.frags -= 1
|
|
296
1321
|
end
|
|
297
1322
|
end
|
|
1323
|
+
|
|
1324
|
+
def apply_player_attacker_obituary_frags(attacker)
|
|
1325
|
+
if attacker.equal?(@player_state)
|
|
1326
|
+
@player_state.frags -= 1
|
|
1327
|
+
elsif teamplay.to_i == 2 &&
|
|
1328
|
+
!quake_info_key(attacker, "team").empty? &&
|
|
1329
|
+
quake_info_key(attacker, "team") == quake_info_key(@player_state, "team")
|
|
1330
|
+
attacker.frags -= 1 if attacker.respond_to?(:frags=)
|
|
1331
|
+
elsif attacker.respond_to?(:frags=)
|
|
1332
|
+
attacker.frags += 1
|
|
1333
|
+
end
|
|
1334
|
+
end
|
|
1335
|
+
|
|
1336
|
+
def attacker_classname(attacker)
|
|
1337
|
+
attacker.respond_to?(:classname) ? attacker.classname : ""
|
|
1338
|
+
end
|
|
1339
|
+
|
|
1340
|
+
def telefrag_owner(attacker)
|
|
1341
|
+
attacker.instance_variable_get(:@owner) if attacker&.instance_variable_defined?(:@owner)
|
|
1342
|
+
end
|
|
1343
|
+
|
|
1344
|
+
def drop_player_backpack
|
|
1345
|
+
return unless @player_state
|
|
1346
|
+
return unless @player
|
|
1347
|
+
|
|
1348
|
+
ammo = @player_state.ammo
|
|
1349
|
+
return unless ammo.values.sum.positive?
|
|
1350
|
+
|
|
1351
|
+
backpack = Entity.new("classname" => "item_backpack", "origin" => backpack_origin_string)
|
|
1352
|
+
backpack.instance_variable_set(:@items, PLAYER_WEAPON_ITEM_BITS.fetch(@player_state.current_weapon, 0))
|
|
1353
|
+
backpack.instance_variable_set(:@netname, PLAYER_WEAPON_NETNAMES.fetch(@player_state.current_weapon, ""))
|
|
1354
|
+
backpack.instance_variable_set(:@ammo_shells, ammo[:shells])
|
|
1355
|
+
backpack.instance_variable_set(:@ammo_nails, ammo[:nails])
|
|
1356
|
+
backpack.instance_variable_set(:@ammo_rockets, ammo[:rockets])
|
|
1357
|
+
backpack.instance_variable_set(:@ammo_cells, ammo[:cells])
|
|
1358
|
+
backpack.velocity = Math::Vec3.new(-100.0 + rand * 200.0, -100.0 + rand * 200.0, 300.0)
|
|
1359
|
+
backpack.flags = 256
|
|
1360
|
+
backpack.instance_variable_set(:@solid, 1)
|
|
1361
|
+
backpack.instance_variable_set(:@movetype, 6)
|
|
1362
|
+
backpack.properties["model"] = "progs/backpack.mdl"
|
|
1363
|
+
backpack.instance_variable_set(:@mins, Math::Vec3.new(-16.0, -16.0, 0.0))
|
|
1364
|
+
backpack.instance_variable_set(:@maxs, Math::Vec3.new(16.0, 16.0, 56.0))
|
|
1365
|
+
backpack.instance_variable_set(:@touch, :backpack_touch)
|
|
1366
|
+
backpack.think_time = @game_time.to_f + 120.0
|
|
1367
|
+
backpack.instance_variable_set(:@think, :sub_remove)
|
|
1368
|
+
@entities << backpack if @entities
|
|
1369
|
+
@item_pickups&.add_entity(backpack)
|
|
1370
|
+
backpack
|
|
1371
|
+
end
|
|
1372
|
+
|
|
1373
|
+
def drop_player_powerups
|
|
1374
|
+
drop_player_powerup(
|
|
1375
|
+
:quad,
|
|
1376
|
+
"dq",
|
|
1377
|
+
"item_artifact_super_damage",
|
|
1378
|
+
"progs/quaddama.mdl",
|
|
1379
|
+
"items/damage.wav",
|
|
1380
|
+
:q_touch
|
|
1381
|
+
)
|
|
1382
|
+
drop_player_powerup(
|
|
1383
|
+
:ring,
|
|
1384
|
+
"dr",
|
|
1385
|
+
"item_artifact_invisibility",
|
|
1386
|
+
"progs/invisibl.mdl",
|
|
1387
|
+
"items/inv1.wav",
|
|
1388
|
+
:r_touch
|
|
1389
|
+
)
|
|
1390
|
+
end
|
|
1391
|
+
|
|
1392
|
+
def drop_player_powerup(powerup, world_key, classname, model, noise, touch)
|
|
1393
|
+
return unless @player_state
|
|
1394
|
+
return unless @player
|
|
1395
|
+
return unless @entities
|
|
1396
|
+
return unless world_info_enabled?(world_key)
|
|
1397
|
+
|
|
1398
|
+
finish_time = @player_state.powerup_finished[powerup]
|
|
1399
|
+
return unless finish_time&.positive?
|
|
1400
|
+
|
|
1401
|
+
item = Entity.new("classname" => classname, "origin" => player_origin_string)
|
|
1402
|
+
item.velocity = Math::Vec3.new(-100.0 + rand * 200.0, -100.0 + rand * 200.0, 300.0)
|
|
1403
|
+
item.flags = 256
|
|
1404
|
+
item.instance_variable_set(:@solid, 1)
|
|
1405
|
+
item.instance_variable_set(:@movetype, MOVETYPE_TOSS)
|
|
1406
|
+
item.properties["model"] = model
|
|
1407
|
+
item.instance_variable_set(:@mins, Math::Vec3.new(-16.0, -16.0, -24.0))
|
|
1408
|
+
item.instance_variable_set(:@maxs, Math::Vec3.new(16.0, 16.0, 32.0))
|
|
1409
|
+
item.instance_variable_set(:@noise, noise)
|
|
1410
|
+
item.instance_variable_set(:@cnt, finish_time.to_f)
|
|
1411
|
+
item.instance_variable_set(:@touch, touch)
|
|
1412
|
+
item.think_time = finish_time.to_f
|
|
1413
|
+
item.instance_variable_set(:@think, :sub_remove)
|
|
1414
|
+
@entities << item
|
|
1415
|
+
@item_pickups&.add_entity(item)
|
|
1416
|
+
item
|
|
1417
|
+
end
|
|
1418
|
+
|
|
1419
|
+
def player_origin_string
|
|
1420
|
+
origin = @player.position
|
|
1421
|
+
"#{origin.x} #{origin.y} #{origin.z}"
|
|
1422
|
+
end
|
|
1423
|
+
|
|
1424
|
+
def backpack_origin_string
|
|
1425
|
+
origin = @player.position - Math::Vec3.new(0.0, 0.0, 24.0)
|
|
1426
|
+
"#{origin.x} #{origin.y} #{origin.z}"
|
|
1427
|
+
end
|
|
1428
|
+
|
|
1429
|
+
def player_eye_position
|
|
1430
|
+
return @player.eye_position unless @player_state&.view_offset
|
|
1431
|
+
|
|
1432
|
+
@player.position + @player_state.view_offset
|
|
1433
|
+
end
|
|
1434
|
+
|
|
1435
|
+
def sync_camera
|
|
1436
|
+
# Mouse look is always active in this port. With +mlook Quake calls
|
|
1437
|
+
# V_StopPitchDrift every frame (in_win.c IN_MouseMove), and the
|
|
1438
|
+
# laststop == cl.time guard in V_StartPitchDrift then keeps the
|
|
1439
|
+
# view from auto-centering while running forward.
|
|
1440
|
+
stop_pitch_drift
|
|
1441
|
+
drift_view_pitch
|
|
1442
|
+
eye = player_eye_position
|
|
1443
|
+
bob = @viewmodel ? @viewmodel.bob : 0.0
|
|
1444
|
+
punch = current_weapon_punch_angle
|
|
1445
|
+
@viewmodel_base_position = Math::Vec3.new(eye.x, eye.y, eye.z + bob)
|
|
1446
|
+
camera_position = bound_view_origin(Math::Vec3.new(
|
|
1447
|
+
eye.x + VIEW_ORIGIN_NUDGE,
|
|
1448
|
+
eye.y + VIEW_ORIGIN_NUDGE,
|
|
1449
|
+
eye.z + bob + VIEW_ORIGIN_NUDGE
|
|
1450
|
+
))
|
|
1451
|
+
stair_offset = stair_smooth_offset
|
|
1452
|
+
idle = current_view_idle_angles
|
|
1453
|
+
@camera.position = Math::Vec3.new(
|
|
1454
|
+
camera_position.x,
|
|
1455
|
+
camera_position.y,
|
|
1456
|
+
camera_position.z + stair_offset
|
|
1457
|
+
)
|
|
1458
|
+
@viewmodel_base_position = Math::Vec3.new(
|
|
1459
|
+
@viewmodel_base_position.x,
|
|
1460
|
+
@viewmodel_base_position.y,
|
|
1461
|
+
@viewmodel_base_position.z + stair_offset
|
|
1462
|
+
)
|
|
1463
|
+
@camera.yaw = @player.yaw + idle.y + punch.y
|
|
1464
|
+
damage_roll, pitch = current_damage_kick_angles
|
|
1465
|
+
@camera.pitch = @player.pitch + pitch + idle.x + punch.x
|
|
1466
|
+
@camera.roll = current_view_roll(damage_roll) + idle.z + punch.z
|
|
1467
|
+
end
|
|
1468
|
+
|
|
1469
|
+
def bound_view_origin(origin)
|
|
1470
|
+
player_origin = @player.position
|
|
1471
|
+
Math::Vec3.new(
|
|
1472
|
+
origin.x.clamp(player_origin.x - VIEW_BOUND_XY, player_origin.x + VIEW_BOUND_XY),
|
|
1473
|
+
origin.y.clamp(player_origin.y - VIEW_BOUND_XY, player_origin.y + VIEW_BOUND_XY),
|
|
1474
|
+
origin.z.clamp(player_origin.z + VIEW_BOUND_Z_MIN, player_origin.z + VIEW_BOUND_Z_MAX)
|
|
1475
|
+
)
|
|
1476
|
+
end
|
|
1477
|
+
|
|
1478
|
+
def stair_smooth_offset
|
|
1479
|
+
oldz = @view_oldz.to_f
|
|
1480
|
+
origin_z = @player.position.z
|
|
1481
|
+
if @player.on_ground && origin_z - oldz > 0.0
|
|
1482
|
+
oldz += [@view_frame_dt.to_f, 0.0].max * STAIR_SMOOTH_SPEED
|
|
1483
|
+
oldz = origin_z if oldz > origin_z
|
|
1484
|
+
oldz = origin_z - STAIR_SMOOTH_MAX if origin_z - oldz > STAIR_SMOOTH_MAX
|
|
1485
|
+
@view_oldz = oldz
|
|
1486
|
+
oldz - origin_z
|
|
1487
|
+
else
|
|
1488
|
+
@view_oldz = origin_z
|
|
1489
|
+
0.0
|
|
1490
|
+
end
|
|
1491
|
+
end
|
|
1492
|
+
|
|
1493
|
+
def current_view_idle_angles
|
|
1494
|
+
scale = @view_idle_scale.to_f
|
|
1495
|
+
return Math::Vec3::ORIGIN if scale.zero?
|
|
1496
|
+
|
|
1497
|
+
time = @game_time.to_f
|
|
1498
|
+
Math::Vec3.new(
|
|
1499
|
+
scale * ::Math.sin(time * VIEW_IDLE_PITCH_CYCLE) * VIEW_IDLE_PITCH_LEVEL,
|
|
1500
|
+
scale * ::Math.sin(time * VIEW_IDLE_YAW_CYCLE) * VIEW_IDLE_YAW_LEVEL,
|
|
1501
|
+
scale * ::Math.sin(time * VIEW_IDLE_ROLL_CYCLE) * VIEW_IDLE_ROLL_LEVEL
|
|
1502
|
+
)
|
|
1503
|
+
end
|
|
1504
|
+
|
|
1505
|
+
def start_pitch_drift
|
|
1506
|
+
return if @view_pitch_laststop.to_f == @game_time.to_f
|
|
1507
|
+
return unless @view_pitch_nodrift || @view_pitch_velocity.to_f.zero?
|
|
1508
|
+
|
|
1509
|
+
@view_pitch_velocity = VIEW_CENTER_SPEED
|
|
1510
|
+
@view_pitch_nodrift = false
|
|
1511
|
+
@view_pitch_driftmove = 0.0
|
|
1512
|
+
end
|
|
1513
|
+
|
|
1514
|
+
def stop_pitch_drift
|
|
1515
|
+
@view_pitch_laststop = @game_time.to_f
|
|
1516
|
+
@view_pitch_nodrift = true
|
|
1517
|
+
@view_pitch_velocity = 0.0
|
|
1518
|
+
end
|
|
1519
|
+
|
|
1520
|
+
def drift_view_pitch
|
|
1521
|
+
return unless @player
|
|
1522
|
+
unless @player.on_ground
|
|
1523
|
+
@view_pitch_driftmove = 0.0
|
|
1524
|
+
@view_pitch_velocity = 0.0
|
|
1525
|
+
return
|
|
1526
|
+
end
|
|
1527
|
+
|
|
1528
|
+
if @view_pitch_nodrift
|
|
1529
|
+
if forward_move_pressed?
|
|
1530
|
+
@view_pitch_driftmove = @view_pitch_driftmove.to_f + @view_frame_dt.to_f
|
|
1531
|
+
start_pitch_drift if @view_pitch_driftmove > VIEW_CENTER_MOVE
|
|
1532
|
+
else
|
|
1533
|
+
@view_pitch_driftmove = 0.0
|
|
1534
|
+
end
|
|
1535
|
+
return
|
|
1536
|
+
end
|
|
1537
|
+
|
|
1538
|
+
delta = @view_idealpitch.to_f - @player.pitch.to_f
|
|
1539
|
+
if delta.zero?
|
|
1540
|
+
@view_pitch_velocity = 0.0
|
|
1541
|
+
return
|
|
1542
|
+
end
|
|
1543
|
+
|
|
1544
|
+
move = @view_frame_dt.to_f * @view_pitch_velocity.to_f
|
|
1545
|
+
@view_pitch_velocity = @view_pitch_velocity.to_f + @view_frame_dt.to_f * VIEW_CENTER_SPEED
|
|
1546
|
+
if delta.positive?
|
|
1547
|
+
if move > delta
|
|
1548
|
+
@view_pitch_velocity = 0.0
|
|
1549
|
+
move = delta
|
|
1550
|
+
end
|
|
1551
|
+
@player.instance_variable_set(:@pitch, @player.pitch + move)
|
|
1552
|
+
else
|
|
1553
|
+
if move > -delta
|
|
1554
|
+
@view_pitch_velocity = 0.0
|
|
1555
|
+
move = -delta
|
|
1556
|
+
end
|
|
1557
|
+
@player.instance_variable_set(:@pitch, @player.pitch - move)
|
|
1558
|
+
end
|
|
1559
|
+
end
|
|
1560
|
+
|
|
1561
|
+
def forward_move_pressed?
|
|
1562
|
+
keys = @keys || {}
|
|
1563
|
+
keys[SDL::SCANCODE_W] || keys[SDL::SCANCODE_S]
|
|
1564
|
+
end
|
|
1565
|
+
|
|
1566
|
+
def viewmodel_camera
|
|
1567
|
+
punch = current_weapon_punch_angle
|
|
1568
|
+
idle = current_view_idle_angles
|
|
1569
|
+
|
|
1570
|
+
camera = Camera.new(
|
|
1571
|
+
position: @viewmodel_base_position || (@camera.position - VIEW_ORIGIN_NUDGE_VECTOR),
|
|
1572
|
+
yaw: @camera.yaw - punch.y - idle.y,
|
|
1573
|
+
pitch: @camera.pitch - punch.x - idle.x,
|
|
1574
|
+
fov: @camera.fov,
|
|
1575
|
+
near: @camera.near,
|
|
1576
|
+
far: @camera.far
|
|
1577
|
+
)
|
|
1578
|
+
camera.roll = @camera.roll - punch.z - idle.z
|
|
1579
|
+
camera
|
|
1580
|
+
end
|
|
1581
|
+
|
|
1582
|
+
def current_view_roll(damage_roll = 0.0)
|
|
1583
|
+
return 80.0 if @player_state&.health.to_i <= 0
|
|
1584
|
+
|
|
1585
|
+
@player.instance_variable_get(:@roll).to_f + movement_view_roll + damage_roll.to_f
|
|
1586
|
+
end
|
|
1587
|
+
|
|
1588
|
+
def movement_view_roll
|
|
1589
|
+
quake_calc_roll(@player.velocity, @player.right)
|
|
1590
|
+
end
|
|
1591
|
+
|
|
1592
|
+
def quake_calc_roll(velocity, right)
|
|
1593
|
+
side = velocity.dot(right)
|
|
1594
|
+
sign = side.negative? ? -1.0 : 1.0
|
|
1595
|
+
side = side.abs
|
|
1596
|
+
value = 2.0
|
|
1597
|
+
side = if side < 200.0
|
|
1598
|
+
side * value / 200.0
|
|
1599
|
+
else
|
|
1600
|
+
value
|
|
1601
|
+
end
|
|
1602
|
+
side * sign
|
|
1603
|
+
end
|
|
1604
|
+
|
|
1605
|
+
def apply_player_death_state(inflictor: nil)
|
|
1606
|
+
@player_state.weapon_model = ""
|
|
1607
|
+
@player_state.view_offset = PlayerState::DEATH_VIEW_OFFSET
|
|
1608
|
+
@player_state.deadflag = PlayerState::DEAD_DYING
|
|
1609
|
+
@player_state.flags &= ~FL_ONGROUND
|
|
1610
|
+
@player&.instance_variable_set(:@model, "progs/player.mdl")
|
|
1611
|
+
@player&.instance_variable_set(:@solid, SOLID_NOT)
|
|
1612
|
+
@player&.instance_variable_set(:@movetype, MOVETYPE_TOSS)
|
|
1613
|
+
@player&.instance_variable_set(:@takedamage, 0)
|
|
1614
|
+
@player&.instance_variable_set(:@touch, nil)
|
|
1615
|
+
@player&.instance_variable_set(:@effects, 0)
|
|
1616
|
+
@player.on_ground = false if @player
|
|
1617
|
+
return apply_player_gib_state(inflictor: inflictor) if @player_state.health < -40
|
|
1618
|
+
@player&.instance_variable_set(:@pitch, 0.0)
|
|
1619
|
+
@player&.instance_variable_set(:@roll, 0.0)
|
|
1620
|
+
return unless @player && @player.velocity.z < 10.0
|
|
1621
|
+
|
|
1622
|
+
@player.velocity = Math::Vec3.new(
|
|
1623
|
+
@player.velocity.x,
|
|
1624
|
+
@player.velocity.y,
|
|
1625
|
+
@player.velocity.z + rand * 300.0
|
|
1626
|
+
)
|
|
1627
|
+
end
|
|
1628
|
+
|
|
1629
|
+
def apply_player_gib_state(inflictor: nil)
|
|
1630
|
+
@player_state.view_offset = PlayerState::GIB_VIEW_OFFSET
|
|
1631
|
+
@player_state.deadflag = PlayerState::DEAD_DEAD
|
|
1632
|
+
@player&.instance_variable_set(:@movetype, MOVETYPE_BOUNCE)
|
|
1633
|
+
return unless @player
|
|
1634
|
+
|
|
1635
|
+
@player.instance_variable_set(:@model, "progs/h_player.mdl")
|
|
1636
|
+
@player.instance_variable_set(:@frame, 0)
|
|
1637
|
+
@player.instance_variable_set(:@nextthink, -1)
|
|
1638
|
+
@player.instance_variable_set(:@mins, Math::Vec3.new(-16.0, -16.0, 0.0))
|
|
1639
|
+
@player.instance_variable_set(:@maxs, Math::Vec3.new(16.0, 16.0, 56.0))
|
|
1640
|
+
@player.instance_variable_set(:@avelocity, Math::Vec3.new(0.0, crandom * 600.0, 0.0))
|
|
1641
|
+
@player.velocity = gib_velocity_for_damage(@player_state.health, inflictor: inflictor)
|
|
1642
|
+
@player.position = @player.position - Math::Vec3.new(0.0, 0.0, 24.0)
|
|
1643
|
+
throw_player_gib("progs/gib1.mdl", inflictor: inflictor)
|
|
1644
|
+
throw_player_gib("progs/gib2.mdl", inflictor: inflictor)
|
|
1645
|
+
throw_player_gib("progs/gib3.mdl", inflictor: inflictor)
|
|
1646
|
+
end
|
|
1647
|
+
|
|
1648
|
+
def select_player_death_animation
|
|
1649
|
+
return unless @player && @player_state.health >= -40
|
|
1650
|
+
|
|
1651
|
+
frames = if @player_state.current_weapon == :axe
|
|
1652
|
+
PLAYER_DEATH_FRAMES.fetch(:axe)
|
|
1653
|
+
else
|
|
1654
|
+
PLAYER_DEATH_FRAMES.fetch(random_player_death_sequence)
|
|
1655
|
+
end
|
|
1656
|
+
@player.instance_variable_set(:@death_animation_frames, frames)
|
|
1657
|
+
@player.instance_variable_set(:@death_animation_index, 0)
|
|
1658
|
+
@player.instance_variable_set(:@frame, frames.fetch(0))
|
|
1659
|
+
@player.instance_variable_set(:@nextthink, @game_time.to_f + 0.1)
|
|
1660
|
+
end
|
|
1661
|
+
|
|
1662
|
+
def random_player_death_sequence
|
|
1663
|
+
forced_sequence = @temp1.to_i
|
|
1664
|
+
sequence = forced_sequence.zero? ? 1 + (rand * 6.0).floor : forced_sequence
|
|
1665
|
+
case sequence
|
|
1666
|
+
when 1 then :a
|
|
1667
|
+
when 2 then :b
|
|
1668
|
+
when 3 then :c
|
|
1669
|
+
when 4 then :d
|
|
1670
|
+
else :e
|
|
1671
|
+
end
|
|
1672
|
+
end
|
|
1673
|
+
|
|
1674
|
+
def update_player_death_animation
|
|
1675
|
+
return unless @player && @player_state
|
|
1676
|
+
return unless @player_state.deadflag == PlayerState::DEAD_DYING
|
|
1677
|
+
|
|
1678
|
+
nextthink = @player.instance_variable_get(:@nextthink)
|
|
1679
|
+
return unless nextthink && nextthink >= 0.0
|
|
1680
|
+
return if nextthink > @game_time
|
|
1681
|
+
|
|
1682
|
+
frames = @player.instance_variable_get(:@death_animation_frames)
|
|
1683
|
+
return unless frames && !frames.empty?
|
|
1684
|
+
|
|
1685
|
+
index = @player.instance_variable_get(:@death_animation_index).to_i + 1
|
|
1686
|
+
index = frames.length - 1 if index >= frames.length
|
|
1687
|
+
@player.instance_variable_set(:@death_animation_index, index)
|
|
1688
|
+
@player.instance_variable_set(:@frame, frames.fetch(index))
|
|
1689
|
+
if index == frames.length - 1
|
|
1690
|
+
player_dead
|
|
1691
|
+
else
|
|
1692
|
+
@player.instance_variable_set(:@nextthink, @game_time.to_f + 0.1)
|
|
1693
|
+
end
|
|
1694
|
+
end
|
|
1695
|
+
|
|
1696
|
+
def player_dead
|
|
1697
|
+
@player_state.deadflag = PlayerState::DEAD_DEAD
|
|
1698
|
+
@player&.instance_variable_set(:@nextthink, -1)
|
|
1699
|
+
end
|
|
1700
|
+
|
|
1701
|
+
def set_suicide_frame
|
|
1702
|
+
return unless @player && @player_state
|
|
1703
|
+
return unless @player.instance_variable_get(:@model) == "progs/player.mdl"
|
|
1704
|
+
|
|
1705
|
+
@player.instance_variable_set(:@frame, PLAYER_DEATHA11_FRAME)
|
|
1706
|
+
@player.instance_variable_set(:@solid, SOLID_NOT)
|
|
1707
|
+
@player.instance_variable_set(:@movetype, MOVETYPE_TOSS)
|
|
1708
|
+
@player_state.deadflag = PlayerState::DEAD_DEAD
|
|
1709
|
+
@player.instance_variable_set(:@nextthink, -1)
|
|
1710
|
+
end
|
|
1711
|
+
|
|
1712
|
+
def client_kill
|
|
1713
|
+
return unless @player && @player_state
|
|
1714
|
+
|
|
1715
|
+
set_suicide_frame
|
|
1716
|
+
@player.instance_variable_set(:@model, "progs/player.mdl")
|
|
1717
|
+
@player_state.frags -= 2
|
|
1718
|
+
respawn_player
|
|
1719
|
+
end
|
|
1720
|
+
|
|
1721
|
+
def update_player_control(dt, solid_brush)
|
|
1722
|
+
return if @player_state.view_offset == Math::Vec3::ORIGIN
|
|
1723
|
+
|
|
1724
|
+
@player_state.deathtype = ""
|
|
1725
|
+
if @player_state.deadflag >= PlayerState::DEAD_DEAD
|
|
1726
|
+
player_death_think
|
|
1727
|
+
return
|
|
1728
|
+
end
|
|
1729
|
+
return if @player_state.deadflag == PlayerState::DEAD_DYING
|
|
1730
|
+
|
|
1731
|
+
play_jump_sounds
|
|
1732
|
+
apply_player_pausetime
|
|
1733
|
+
@player.update(dt, @level, quake_player_movement_keys, brush_entities: solid_brush)
|
|
1734
|
+
sync_water_jump_flag
|
|
1735
|
+
apply_fall_damage
|
|
1736
|
+
apply_environment_damage(dt)
|
|
1737
|
+
end
|
|
1738
|
+
|
|
1739
|
+
# Mirror the physics player's FL_WATERJUMP state into the QuakeC-style
|
|
1740
|
+
# player flags so jump sounds and water drag see it.
|
|
1741
|
+
def sync_water_jump_flag
|
|
1742
|
+
if @player.water_jump
|
|
1743
|
+
@player_state.flags |= PlayerState::FL_WATERJUMP
|
|
1744
|
+
else
|
|
1745
|
+
@player_state.flags &= ~PlayerState::FL_WATERJUMP
|
|
1746
|
+
end
|
|
1747
|
+
end
|
|
1748
|
+
|
|
1749
|
+
def player_postthink_active?
|
|
1750
|
+
@player_state.view_offset != Math::Vec3::ORIGIN &&
|
|
1751
|
+
@player_state.deadflag == PlayerState::DEAD_NO
|
|
1752
|
+
end
|
|
1753
|
+
|
|
1754
|
+
def apply_player_pausetime
|
|
1755
|
+
return unless @game_time.to_f < @player.instance_variable_get(:@pausetime).to_f
|
|
1756
|
+
|
|
1757
|
+
@player.velocity = Math::Vec3::ORIGIN
|
|
1758
|
+
end
|
|
1759
|
+
|
|
1760
|
+
def quake_player_movement_keys
|
|
1761
|
+
keys = @keys || {}
|
|
1762
|
+
keys = quake_jump_movement_keys(keys)
|
|
1763
|
+
teleport_time = @player.instance_variable_get(:@teleport_time).to_f
|
|
1764
|
+
return keys unless @game_time.to_f < teleport_time
|
|
1765
|
+
return keys if keys[SDL::SCANCODE_W] || !keys[SDL::SCANCODE_S]
|
|
1766
|
+
|
|
1767
|
+
keys.merge(SDL::SCANCODE_S => false)
|
|
1768
|
+
end
|
|
1769
|
+
|
|
1770
|
+
def quake_jump_movement_keys(keys)
|
|
1771
|
+
return keys unless @player_state
|
|
1772
|
+
|
|
1773
|
+
jump_pressed = keys[SDL::SCANCODE_SPACE]
|
|
1774
|
+
unless jump_pressed
|
|
1775
|
+
@player_state.flags |= PlayerState::FL_JUMPRELEASED
|
|
1776
|
+
return keys
|
|
1777
|
+
end
|
|
1778
|
+
|
|
1779
|
+
return keys if @player.water_level >= 2
|
|
1780
|
+
return keys unless @player.on_ground
|
|
1781
|
+
|
|
1782
|
+
if (@player_state.flags & PlayerState::FL_JUMPRELEASED).zero?
|
|
1783
|
+
return keys.merge(SDL::SCANCODE_SPACE => false)
|
|
1784
|
+
end
|
|
1785
|
+
|
|
1786
|
+
@player_state.flags &= ~PlayerState::FL_JUMPRELEASED
|
|
1787
|
+
keys
|
|
1788
|
+
end
|
|
1789
|
+
|
|
1790
|
+
def player_death_think
|
|
1791
|
+
apply_player_death_friction if @player.on_ground
|
|
1792
|
+
if @player_state.deadflag == PlayerState::DEAD_DEAD
|
|
1793
|
+
return if player_death_button_down?
|
|
1794
|
+
|
|
1795
|
+
@player_state.deadflag = PlayerState::DEAD_RESPAWNABLE
|
|
1796
|
+
return
|
|
1797
|
+
end
|
|
1798
|
+
|
|
1799
|
+
return unless player_death_button_down?
|
|
1800
|
+
|
|
1801
|
+
@attack_pressed = false
|
|
1802
|
+
@keys.delete(SDL::SCANCODE_SPACE) if defined?(SDL::SCANCODE_SPACE)
|
|
1803
|
+
respawn_player
|
|
1804
|
+
end
|
|
1805
|
+
|
|
1806
|
+
def respawn_player
|
|
1807
|
+
copy_player_to_body_queue
|
|
1808
|
+
@player_state.reset_to_new_parms
|
|
1809
|
+
@attack_finished = @game_time
|
|
1810
|
+
@air_finished = @game_time + 12.0
|
|
1811
|
+
@drown_damage = 2.0
|
|
1812
|
+
@drown_pain_finished = 0.0
|
|
1813
|
+
@player_in_water = false
|
|
1814
|
+
@jump_flag = 0.0
|
|
1815
|
+
|
|
1816
|
+
set_player_spawn_point(select_spawn_point)
|
|
1817
|
+
@player.position = player_spawn_origin
|
|
1818
|
+
@player.instance_variable_set(:@yaw, (@player_yaw || 0.0).to_f)
|
|
1819
|
+
apply_put_client_in_server_spawn_effects
|
|
1820
|
+
@player.velocity = Math::Vec3::ORIGIN
|
|
1821
|
+
@player.on_ground = false
|
|
1822
|
+
@player.water_level = 0
|
|
1823
|
+
@player.water_type = Physics::CONTENTS_EMPTY
|
|
1824
|
+
@player.instance_variable_set(:@fixangle, true)
|
|
1825
|
+
@player.instance_variable_set(:@pitch, 0.0)
|
|
1826
|
+
@player.instance_variable_set(:@roll, 0.0)
|
|
1827
|
+
set_player_spawn_entity_state
|
|
1828
|
+
@viewmodel&.set_weapon(@player_state.current_weapon_model)
|
|
1829
|
+
sync_camera if @camera
|
|
1830
|
+
end
|
|
1831
|
+
|
|
1832
|
+
def set_player_spawn_point(spot)
|
|
1833
|
+
return unless spot
|
|
1834
|
+
|
|
1835
|
+
@player_start = spot&.position || Math::Vec3::ORIGIN
|
|
1836
|
+
@player_yaw = spot ? spot.angles.y : 0.0
|
|
1837
|
+
end
|
|
1838
|
+
|
|
1839
|
+
def player_spawn_origin
|
|
1840
|
+
(@player_start || Math::Vec3::ORIGIN) + Math::Vec3.new(0.0, 0.0, 1.0)
|
|
1841
|
+
end
|
|
1842
|
+
|
|
1843
|
+
# SelectSpawnPoint (client.qc): deathmatch spots only in deathmatch;
|
|
1844
|
+
# single player uses info_player_start2 on episode returns
|
|
1845
|
+
# (serverflags set), else info_player_start.
|
|
1846
|
+
def select_spawn_point
|
|
1847
|
+
test_start = @entities&.find { |ent| ent.classname == "testplayerstart" }
|
|
1848
|
+
return test_start if test_start
|
|
1849
|
+
|
|
1850
|
+
if @deathmatch.to_i.positive?
|
|
1851
|
+
deathmatch_spots = @entities&.select { |ent| ent.classname == "info_player_deathmatch" } || []
|
|
1852
|
+
return select_deathmatch_spawn_point(deathmatch_spots) unless deathmatch_spots.empty?
|
|
1853
|
+
end
|
|
1854
|
+
|
|
1855
|
+
if @player_state&.serverflags.to_i.positive?
|
|
1856
|
+
episode_return = @entities&.find { |ent| ent.classname == "info_player_start2" }
|
|
1857
|
+
return episode_return if episode_return
|
|
1858
|
+
end
|
|
1859
|
+
|
|
1860
|
+
@entities&.find { |ent| ent.classname == "info_player_start" }
|
|
1861
|
+
end
|
|
1862
|
+
|
|
1863
|
+
def select_deathmatch_spawn_point(spots)
|
|
1864
|
+
open_spots = spots.select { |spot| player_count_near_spawn(spot).zero? }
|
|
1865
|
+
return random_spawn_spot(spots) if open_spots.empty?
|
|
1866
|
+
|
|
1867
|
+
random_spawn_spot(open_spots.reverse)
|
|
1868
|
+
end
|
|
1869
|
+
|
|
1870
|
+
def player_count_near_spawn(spot)
|
|
1871
|
+
(@entities || []).count do |ent|
|
|
1872
|
+
ent.classname == "player" && (ent.position - spot.position).length <= 84.0
|
|
1873
|
+
end
|
|
1874
|
+
end
|
|
1875
|
+
|
|
1876
|
+
def random_spawn_spot(spots)
|
|
1877
|
+
return nil if spots.empty?
|
|
1878
|
+
|
|
1879
|
+
index = (rand * (spots.length - 1)).round
|
|
1880
|
+
spots.fetch(index)
|
|
1881
|
+
end
|
|
1882
|
+
|
|
1883
|
+
def set_player_spawn_entity_state
|
|
1884
|
+
@player.instance_variable_set(:@model, "progs/player.mdl")
|
|
1885
|
+
frames = @player_state.current_weapon == :axe ? PLAYER_STAND_FRAMES.fetch(:axe) : PLAYER_STAND_FRAMES.fetch(:normal)
|
|
1886
|
+
@player.instance_variable_set(:@frame, frames.fetch(0))
|
|
1887
|
+
@player.instance_variable_set(:@walkframe, 1)
|
|
1888
|
+
@player.instance_variable_set(:@weaponframe, 0)
|
|
1889
|
+
@player.instance_variable_set(:@solid, SOLID_SLIDEBOX)
|
|
1890
|
+
@player.instance_variable_set(:@movetype, MOVETYPE_WALK)
|
|
1891
|
+
@player.instance_variable_set(:@takedamage, DAMAGE_AIM)
|
|
1892
|
+
@player.instance_variable_set(:@effects, 0)
|
|
1893
|
+
@player.instance_variable_set(:@pausetime, 0.0)
|
|
1894
|
+
@player.instance_variable_set(:@mins, ENTITY_HULL_MINS)
|
|
1895
|
+
@player.instance_variable_set(:@maxs, ENTITY_HULL_MAXS)
|
|
1896
|
+
@player.instance_variable_set(:@nextthink, nil)
|
|
1897
|
+
@player.instance_variable_set(:@death_animation_frames, nil)
|
|
1898
|
+
@player.instance_variable_set(:@pain_animation_frames, nil)
|
|
1899
|
+
end
|
|
1900
|
+
|
|
1901
|
+
def apply_put_client_in_server_spawn_effects
|
|
1902
|
+
yaw = (@player_yaw || 0.0).to_f
|
|
1903
|
+
spawn_teleport_fog(@player.position + angle_forward(Math::Vec3.new(0.0, yaw, 0.0)) * 20.0)
|
|
1904
|
+
apply_spawn_telefrag_damage(@player.position)
|
|
1905
|
+
apply_world_spawn_modifiers
|
|
1906
|
+
apply_deathmatch_spawn_parms if @player_state.alive?
|
|
1907
|
+
end
|
|
1908
|
+
|
|
1909
|
+
def apply_world_spawn_modifiers
|
|
1910
|
+
world_rj = world_info_value("rj")
|
|
1911
|
+
@rj = world_rj.to_f if world_rj && world_rj.to_f.nonzero?
|
|
1912
|
+
end
|
|
1913
|
+
|
|
1914
|
+
def apply_deathmatch_spawn_parms
|
|
1915
|
+
case deathmatch.to_i
|
|
1916
|
+
when 4
|
|
1917
|
+
@player_state.health = 250
|
|
1918
|
+
@player_state.armor = 200
|
|
1919
|
+
@player_state.armor_type = 3
|
|
1920
|
+
@player_state.ammo[:shells] = 0
|
|
1921
|
+
if !deathmatch_four_axe_mode?
|
|
1922
|
+
@player_state.ammo[:shells] = 255
|
|
1923
|
+
@player_state.ammo[:nails] = 255
|
|
1924
|
+
@player_state.ammo[:rockets] = 255
|
|
1925
|
+
@player_state.ammo[:cells] = 255
|
|
1926
|
+
@player_state.weapons_owned.merge(
|
|
1927
|
+
%i[nailgun super_nailgun super_shotgun rocket_launcher lightning_gun]
|
|
1928
|
+
)
|
|
1929
|
+
end
|
|
1930
|
+
@player_state.add_powerup(:pentagram, game_time: @game_time, finish_time: @game_time.to_f + 3.0)
|
|
1931
|
+
when 5
|
|
1932
|
+
@player_state.health = 200
|
|
1933
|
+
@player_state.armor = 200
|
|
1934
|
+
@player_state.armor_type = 3
|
|
1935
|
+
@player_state.ammo[:shells] = 30
|
|
1936
|
+
@player_state.ammo[:nails] = 80
|
|
1937
|
+
@player_state.ammo[:rockets] = 10
|
|
1938
|
+
@player_state.ammo[:cells] = 30
|
|
1939
|
+
@player_state.weapons_owned.merge(
|
|
1940
|
+
%i[nailgun super_nailgun super_shotgun rocket_launcher grenade_launcher lightning_gun]
|
|
1941
|
+
)
|
|
1942
|
+
@player_state.add_powerup(:pentagram, game_time: @game_time, finish_time: @game_time.to_f + 3.0)
|
|
1943
|
+
end
|
|
1944
|
+
end
|
|
1945
|
+
|
|
1946
|
+
def deathmatch_four_axe_mode?
|
|
1947
|
+
axe.to_f.nonzero? || world_info_enabled?("axe")
|
|
1948
|
+
end
|
|
1949
|
+
|
|
1950
|
+
def apply_spawn_telefrag_damage(origin)
|
|
1951
|
+
apply_telefrag_damage(origin)
|
|
1952
|
+
end
|
|
1953
|
+
|
|
1954
|
+
def copy_player_to_body_queue
|
|
1955
|
+
initialize_body_queue
|
|
1956
|
+
body = @body_queue.fetch(@body_queue_index)
|
|
1957
|
+
body.angles = Math::Vec3.new(@player.pitch, @player.yaw, @player.instance_variable_get(:@roll).to_f)
|
|
1958
|
+
body.properties["model"] = @player.instance_variable_get(:@model).to_s
|
|
1959
|
+
body.model_index = player_model_index
|
|
1960
|
+
body.frame = @player.instance_variable_get(:@frame).to_i
|
|
1961
|
+
body.instance_variable_set(:@colormap, @player.instance_variable_get(:@colormap))
|
|
1962
|
+
body.instance_variable_set(:@movetype, @player.instance_variable_get(:@movetype))
|
|
1963
|
+
body.velocity = @player.velocity
|
|
1964
|
+
body.flags = 0
|
|
1965
|
+
body.position = @player.position
|
|
1966
|
+
body.instance_variable_set(:@mins, @player.instance_variable_get(:@mins))
|
|
1967
|
+
body.instance_variable_set(:@maxs, @player.instance_variable_get(:@maxs))
|
|
1968
|
+
@body_queue_index = (@body_queue_index + 1) % @body_queue.length
|
|
1969
|
+
body
|
|
1970
|
+
end
|
|
1971
|
+
|
|
1972
|
+
def player_model_index
|
|
1973
|
+
return @player.model_index if @player.respond_to?(:model_index)
|
|
1974
|
+
|
|
1975
|
+
@player.instance_variable_get(:@model_index)
|
|
1976
|
+
end
|
|
1977
|
+
|
|
1978
|
+
def initialize_body_queue
|
|
1979
|
+
return if @body_queue && @body_queue.all? { |body| @entities&.include?(body) }
|
|
1980
|
+
|
|
1981
|
+
@body_queue = 4.times.map { Entity.new("classname" => "bodyque") }
|
|
1982
|
+
@body_queue.each_with_index do |body, index|
|
|
1983
|
+
body.instance_variable_set(:@owner, @body_queue[(index + 1) % @body_queue.length])
|
|
1984
|
+
end
|
|
1985
|
+
@body_queue_index = 0
|
|
1986
|
+
@entities ||= []
|
|
1987
|
+
@entities.concat(@body_queue)
|
|
1988
|
+
end
|
|
1989
|
+
|
|
1990
|
+
def apply_player_death_friction
|
|
1991
|
+
speed = @player.velocity.length - 20.0
|
|
1992
|
+
@player.velocity = if speed <= 0.0
|
|
1993
|
+
Math::Vec3::ORIGIN
|
|
1994
|
+
else
|
|
1995
|
+
@player.velocity.normalize * speed
|
|
1996
|
+
end
|
|
1997
|
+
end
|
|
1998
|
+
|
|
1999
|
+
def player_death_button_down?
|
|
2000
|
+
@attack_pressed || (@keys || {})[SDL::SCANCODE_SPACE]
|
|
2001
|
+
end
|
|
2002
|
+
|
|
2003
|
+
def select_player_pain_animation
|
|
2004
|
+
return unless @player && @player_state
|
|
2005
|
+
return unless @player_state.deadflag == PlayerState::DEAD_NO
|
|
2006
|
+
return if @player.instance_variable_get(:@weaponframe).to_i.positive?
|
|
2007
|
+
return if @player_state.powerup_active?(:ring, game_time: @game_time)
|
|
2008
|
+
|
|
2009
|
+
frames = if @player_state.current_weapon == :axe
|
|
2010
|
+
PLAYER_PAIN_FRAMES.fetch(:axe)
|
|
2011
|
+
else
|
|
2012
|
+
PLAYER_PAIN_FRAMES.fetch(:normal)
|
|
2013
|
+
end
|
|
2014
|
+
@player.instance_variable_set(:@pain_animation_frames, frames)
|
|
2015
|
+
@player.instance_variable_set(:@pain_animation_index, 0)
|
|
2016
|
+
@player.instance_variable_set(:@frame, frames.fetch(0))
|
|
2017
|
+
@player.instance_variable_set(:@weaponframe, 0)
|
|
2018
|
+
@player.instance_variable_set(:@pain_nextthink, @game_time.to_f + 0.1)
|
|
2019
|
+
end
|
|
2020
|
+
|
|
2021
|
+
def update_player_pain_animation
|
|
2022
|
+
return unless @player && @player_state
|
|
2023
|
+
return unless @player_state.deadflag == PlayerState::DEAD_NO
|
|
2024
|
+
|
|
2025
|
+
nextthink = @player.instance_variable_get(:@pain_nextthink)
|
|
2026
|
+
return unless nextthink
|
|
2027
|
+
return if nextthink > @game_time
|
|
2028
|
+
|
|
2029
|
+
frames = @player.instance_variable_get(:@pain_animation_frames)
|
|
2030
|
+
return unless frames && !frames.empty?
|
|
2031
|
+
|
|
2032
|
+
index = @player.instance_variable_get(:@pain_animation_index).to_i + 1
|
|
2033
|
+
if index >= frames.length
|
|
2034
|
+
@player.instance_variable_set(:@pain_animation_frames, nil)
|
|
2035
|
+
@player.instance_variable_set(:@pain_animation_index, nil)
|
|
2036
|
+
@player.instance_variable_set(:@pain_nextthink, nil)
|
|
2037
|
+
return
|
|
2038
|
+
end
|
|
2039
|
+
|
|
2040
|
+
@player.instance_variable_set(:@pain_animation_index, index)
|
|
2041
|
+
@player.instance_variable_set(:@frame, frames.fetch(index))
|
|
2042
|
+
@player.instance_variable_set(:@pain_nextthink, @game_time.to_f + 0.1)
|
|
2043
|
+
end
|
|
2044
|
+
|
|
2045
|
+
def gib_velocity_for_damage(damage, inflictor: nil)
|
|
2046
|
+
velocity = if moving_damage_inflictor?(inflictor)
|
|
2047
|
+
base = inflictor.velocity * 0.5
|
|
2048
|
+
push = (@player.position - inflictor.position).normalize * 25.0
|
|
2049
|
+
velocity = base + push
|
|
2050
|
+
z = 100.0 + 240.0 * rand
|
|
2051
|
+
Math::Vec3.new(velocity.x + 200.0 * crandom, velocity.y + 200.0 * crandom, z)
|
|
2052
|
+
else
|
|
2053
|
+
Math::Vec3.new(100.0 * crandom, 100.0 * crandom, 200.0 + 100.0 * rand)
|
|
2054
|
+
end
|
|
2055
|
+
scale = if damage > -50
|
|
2056
|
+
0.7
|
|
2057
|
+
elsif damage > -200
|
|
2058
|
+
2.0
|
|
2059
|
+
else
|
|
2060
|
+
10.0
|
|
2061
|
+
end
|
|
2062
|
+
velocity * scale
|
|
2063
|
+
end
|
|
2064
|
+
|
|
2065
|
+
def moving_damage_inflictor?(inflictor)
|
|
2066
|
+
@player && inflictor&.respond_to?(:velocity) && inflictor.respond_to?(:position) &&
|
|
2067
|
+
inflictor.velocity.length.positive?
|
|
2068
|
+
end
|
|
2069
|
+
|
|
2070
|
+
def throw_player_gib(model, inflictor: nil)
|
|
2071
|
+
return unless @entities && @player_state && @player
|
|
2072
|
+
|
|
2073
|
+
gib = Entity.new("origin" => vec3_string(@player.position), "model" => model)
|
|
2074
|
+
gib.instance_variable_set(:@mins, Math::Vec3::ORIGIN)
|
|
2075
|
+
gib.instance_variable_set(:@maxs, Math::Vec3::ORIGIN)
|
|
2076
|
+
gib.velocity = gib_velocity_for_damage(@player_state.health, inflictor: inflictor)
|
|
2077
|
+
gib.instance_variable_set(:@movetype, MOVETYPE_BOUNCE)
|
|
2078
|
+
gib.instance_variable_set(:@solid, SOLID_NOT)
|
|
2079
|
+
gib.instance_variable_set(:@avelocity, Math::Vec3.new(rand * 600.0, rand * 600.0, rand * 600.0))
|
|
2080
|
+
gib.think_time = @game_time.to_f + 10.0 + rand * 10.0
|
|
2081
|
+
gib.frame = 0
|
|
2082
|
+
gib.flags = 0
|
|
2083
|
+
gib.instance_variable_set(:@think, :sub_remove)
|
|
2084
|
+
@entities << gib
|
|
2085
|
+
gib
|
|
2086
|
+
end
|
|
2087
|
+
|
|
2088
|
+
def spawn_death_bubbles(count)
|
|
2089
|
+
return unless @entities && @player
|
|
2090
|
+
|
|
2091
|
+
spawner = Entity.new("origin" => vec3_string(@player.position))
|
|
2092
|
+
spawner.instance_variable_set(:@movetype, MOVETYPE_NONE)
|
|
2093
|
+
spawner.instance_variable_set(:@solid, SOLID_NOT)
|
|
2094
|
+
spawner.think_time = @game_time.to_f + 0.1
|
|
2095
|
+
spawner.instance_variable_set(:@think, :death_bubbles_spawn)
|
|
2096
|
+
spawner.instance_variable_set(:@air_finished, 0)
|
|
2097
|
+
spawner.instance_variable_set(:@owner, @player)
|
|
2098
|
+
spawner.instance_variable_set(:@bubble_count, count)
|
|
2099
|
+
@entities << spawner
|
|
2100
|
+
spawner
|
|
2101
|
+
end
|
|
2102
|
+
|
|
2103
|
+
def spawn_death_bubble(owner)
|
|
2104
|
+
spawn_bubble(
|
|
2105
|
+
origin: owner.position + Math::Vec3.new(0.0, 0.0, 24.0),
|
|
2106
|
+
velocity: Math::Vec3.new(0.0, 0.0, 15.0),
|
|
2107
|
+
frame: 0,
|
|
2108
|
+
cnt: 0,
|
|
2109
|
+
waterlevel: owner.water_level,
|
|
2110
|
+
touch: nil
|
|
2111
|
+
)
|
|
2112
|
+
end
|
|
2113
|
+
|
|
2114
|
+
def spawn_bubble(origin:, velocity:, frame:, cnt:, waterlevel: 0, touch: :bubble_remove)
|
|
2115
|
+
bubble = Entity.new(
|
|
2116
|
+
"classname" => "bubble",
|
|
2117
|
+
"origin" => vec3_string(origin),
|
|
2118
|
+
"model" => "progs/s_bubble.spr"
|
|
2119
|
+
)
|
|
2120
|
+
bubble.instance_variable_set(:@movetype, MOVETYPE_NOCLIP)
|
|
2121
|
+
bubble.instance_variable_set(:@solid, SOLID_NOT)
|
|
2122
|
+
bubble.velocity = velocity
|
|
2123
|
+
bubble.think_time = @game_time.to_f + 0.5
|
|
2124
|
+
bubble.instance_variable_set(:@think, :bubble_bob)
|
|
2125
|
+
bubble.instance_variable_set(:@touch, touch)
|
|
2126
|
+
bubble.frame = frame
|
|
2127
|
+
bubble.instance_variable_set(:@cnt, cnt)
|
|
2128
|
+
bubble.instance_variable_set(:@waterlevel, waterlevel.to_i)
|
|
2129
|
+
bubble.instance_variable_set(:@mins, Math::Vec3.new(-8.0, -8.0, -8.0))
|
|
2130
|
+
bubble.instance_variable_set(:@maxs, Math::Vec3.new(8.0, 8.0, 8.0))
|
|
2131
|
+
@entities << bubble
|
|
2132
|
+
bubble
|
|
2133
|
+
end
|
|
2134
|
+
|
|
2135
|
+
def update_death_bubble_spawner(ent)
|
|
2136
|
+
return if ent.think_time > @game_time
|
|
2137
|
+
|
|
2138
|
+
owner = ent.instance_variable_get(:@owner)
|
|
2139
|
+
return unless owner&.water_level == 3
|
|
2140
|
+
|
|
2141
|
+
spawn_death_bubble(owner)
|
|
2142
|
+
ent.think_time = @game_time.to_f + 0.1
|
|
2143
|
+
ent.instance_variable_set(:@think, :death_bubbles_spawn)
|
|
2144
|
+
air_finished = ent.instance_variable_get(:@air_finished).to_i + 1
|
|
2145
|
+
ent.instance_variable_set(:@air_finished, air_finished)
|
|
2146
|
+
if air_finished >= ent.instance_variable_get(:@bubble_count).to_i
|
|
2147
|
+
ent.instance_variable_set(:@removed, true)
|
|
2148
|
+
end
|
|
2149
|
+
end
|
|
2150
|
+
|
|
2151
|
+
def update_bubble_bob(ent)
|
|
2152
|
+
return if ent.think_time > @game_time
|
|
2153
|
+
|
|
2154
|
+
cnt = ent.instance_variable_get(:@cnt).to_i + 1
|
|
2155
|
+
ent.instance_variable_set(:@cnt, cnt)
|
|
2156
|
+
if cnt == 4
|
|
2157
|
+
waterlevel = ent.instance_variable_get(:@waterlevel).to_i
|
|
2158
|
+
spawn_bubble(origin: ent.position, velocity: ent.velocity, frame: 1, cnt: 10, waterlevel: waterlevel)
|
|
2159
|
+
ent.frame = 1
|
|
2160
|
+
ent.instance_variable_set(:@cnt, 10)
|
|
2161
|
+
ent.instance_variable_set(:@removed, true) if waterlevel != 3
|
|
2162
|
+
end
|
|
2163
|
+
if ent.instance_variable_get(:@cnt).to_i == 20
|
|
2164
|
+
ent.instance_variable_set(:@removed, true)
|
|
2165
|
+
end
|
|
2166
|
+
|
|
2167
|
+
rnd1 = ent.velocity.x + (-10.0 + rand * 20.0)
|
|
2168
|
+
rnd2 = ent.velocity.y + (-10.0 + rand * 20.0)
|
|
2169
|
+
rnd3 = ent.velocity.z + 10.0 + rand * 10.0
|
|
2170
|
+
rnd1 = 5.0 if rnd1 > 10.0
|
|
2171
|
+
rnd1 = -5.0 if rnd1 < -10.0
|
|
2172
|
+
rnd2 = 5.0 if rnd2 > 10.0
|
|
2173
|
+
rnd2 = -5.0 if rnd2 < -10.0
|
|
2174
|
+
rnd3 = 15.0 if rnd3 < 10.0
|
|
2175
|
+
rnd3 = 25.0 if rnd3 > 30.0
|
|
2176
|
+
ent.velocity = Math::Vec3.new(rnd1, rnd2, rnd3)
|
|
2177
|
+
ent.think_time = @game_time.to_f + 0.5
|
|
2178
|
+
ent.instance_variable_set(:@think, :bubble_bob)
|
|
2179
|
+
end
|
|
2180
|
+
|
|
2181
|
+
def update_temporary_entities
|
|
2182
|
+
@temporary_beams&.reject! { |beam| beam.end_time < @game_time.to_f }
|
|
2183
|
+
return unless @entities
|
|
2184
|
+
|
|
2185
|
+
@entities.each do |ent|
|
|
2186
|
+
next if ent.removed?
|
|
2187
|
+
case ent.instance_variable_get(:@think)
|
|
2188
|
+
when :play_teleport
|
|
2189
|
+
next if ent.think_time > @game_time
|
|
2190
|
+
|
|
2191
|
+
@sound_events&.on_teleport_fog(teleport_fog_sound)
|
|
2192
|
+
ent.instance_variable_set(:@removed, true)
|
|
2193
|
+
next
|
|
2194
|
+
when :death_bubbles_spawn
|
|
2195
|
+
update_death_bubble_spawner(ent)
|
|
2196
|
+
next
|
|
2197
|
+
when :bubble_bob
|
|
2198
|
+
update_bubble_bob(ent)
|
|
2199
|
+
next
|
|
2200
|
+
end
|
|
2201
|
+
|
|
2202
|
+
next unless ent.instance_variable_get(:@think) == :sub_remove
|
|
2203
|
+
next if ent.think_time > @game_time
|
|
2204
|
+
|
|
2205
|
+
ent.instance_variable_set(:@removed, true)
|
|
2206
|
+
end
|
|
2207
|
+
end
|
|
2208
|
+
|
|
2209
|
+
def teleport_fog_sound
|
|
2210
|
+
index = (rand * 5.0).floor + 1
|
|
2211
|
+
index = [[index, 1].max, 5].min
|
|
2212
|
+
"misc/r_tele#{index}.wav"
|
|
2213
|
+
end
|
|
2214
|
+
|
|
2215
|
+
def vec3_string(vec)
|
|
2216
|
+
"#{vec.x} #{vec.y} #{vec.z}"
|
|
2217
|
+
end
|
|
2218
|
+
|
|
2219
|
+
def vec3_property(ent, key)
|
|
2220
|
+
raw = ent[key]
|
|
2221
|
+
return Math::Vec3::ORIGIN unless raw
|
|
2222
|
+
|
|
2223
|
+
parts = raw.split.first(3).map(&:to_f)
|
|
2224
|
+
parts << 0.0 while parts.length < 3
|
|
2225
|
+
Math::Vec3.new(parts[0], parts[1], parts[2])
|
|
2226
|
+
end
|
|
2227
|
+
|
|
2228
|
+
def check_powerup_warnings
|
|
2229
|
+
return if @player_state.health <= 0
|
|
2230
|
+
|
|
2231
|
+
play_ring_idle_sound
|
|
2232
|
+
POWERUP_EXPIRING_EVENTS.each do |powerup, event|
|
|
2233
|
+
finished = @player_state.powerup_finished[powerup]
|
|
2234
|
+
next unless finished
|
|
2235
|
+
next unless finished < @game_time + 3.0
|
|
2236
|
+
|
|
2237
|
+
warning_time = @player_state.powerup_warning_time[powerup].to_f
|
|
2238
|
+
if warning_time == 1.0
|
|
2239
|
+
@player_state.powerup_warning_time[powerup] = @game_time.to_f + 1.0
|
|
2240
|
+
sprint(powerup_expiring_message(powerup))
|
|
2241
|
+
trigger_bonus_flash
|
|
2242
|
+
@sound_events&.on_player(event)
|
|
2243
|
+
elsif warning_time < @game_time
|
|
2244
|
+
@player_state.powerup_warning_time[powerup] = @game_time.to_f + 1.0
|
|
2245
|
+
trigger_bonus_flash
|
|
2246
|
+
end
|
|
2247
|
+
end
|
|
2248
|
+
end
|
|
2249
|
+
|
|
2250
|
+
# client.qc CheckPowerups sprints; deathmatch 4 renames the quad to
|
|
2251
|
+
# OctaPower.
|
|
2252
|
+
def powerup_expiring_message(powerup)
|
|
2253
|
+
return "OctaPower is wearing off" if powerup == :quad && @deathmatch.to_i == 4
|
|
2254
|
+
|
|
2255
|
+
POWERUP_EXPIRING_MESSAGES[powerup]
|
|
2256
|
+
end
|
|
2257
|
+
|
|
2258
|
+
def play_ring_idle_sound
|
|
2259
|
+
return unless @player_state.powerup_finished[:ring]
|
|
2260
|
+
return unless (@ring_idle_sound_finished || 0.0) < @game_time
|
|
2261
|
+
|
|
2262
|
+
@ring_idle_sound_finished = @game_time + (rand * 3.0) + 1.0
|
|
2263
|
+
@sound_events&.on_player(:ring_idle)
|
|
2264
|
+
end
|
|
2265
|
+
|
|
2266
|
+
def refresh_biosuit_air
|
|
2267
|
+
return unless @player_state.powerup_finished[:biosuit]
|
|
2268
|
+
|
|
2269
|
+
@air_finished = @game_time + 12.0
|
|
2270
|
+
end
|
|
2271
|
+
|
|
2272
|
+
def update_player_powerup_visual_state
|
|
2273
|
+
return unless @player && @player_state
|
|
2274
|
+
return if @player_state.health <= 0
|
|
2275
|
+
|
|
2276
|
+
if @player_state.powerup_finished[:ring]
|
|
2277
|
+
@player.instance_variable_set(:@model, "progs/eyes.mdl")
|
|
2278
|
+
@player.instance_variable_set(:@frame, 0)
|
|
2279
|
+
else
|
|
2280
|
+
@player.instance_variable_set(:@model, "progs/player.mdl")
|
|
2281
|
+
end
|
|
2282
|
+
end
|
|
2283
|
+
|
|
2284
|
+
def update_player_powerups
|
|
2285
|
+
apply_deathmatch_four_quad_expiry
|
|
2286
|
+
@player_state.update_powerups(@game_time)
|
|
2287
|
+
end
|
|
2288
|
+
|
|
2289
|
+
def apply_deathmatch_four_quad_expiry
|
|
2290
|
+
return unless deathmatch.to_i == 4
|
|
2291
|
+
|
|
2292
|
+
finished = @player_state.powerup_finished[:quad]
|
|
2293
|
+
return unless finished && finished < @game_time.to_f
|
|
2294
|
+
|
|
2295
|
+
@player_state.ammo[:cells] = 255
|
|
2296
|
+
@player_state.armor = 1
|
|
2297
|
+
@player_state.armor_type = 3
|
|
2298
|
+
@player_state.health = 100
|
|
2299
|
+
end
|
|
2300
|
+
|
|
2301
|
+
def check_no_ammo_weapon_switch
|
|
2302
|
+
return unless @player_state.alive?
|
|
2303
|
+
return unless @game_time > @attack_finished
|
|
2304
|
+
return if @player_state.current_weapon == :axe
|
|
2305
|
+
return unless @player_state.current_ammo_count.to_i.zero?
|
|
2306
|
+
|
|
2307
|
+
previous = @player_state.current_weapon
|
|
2308
|
+
@player_state.switch_to_best_weapon(water_level: @player&.water_level.to_i)
|
|
2309
|
+
@viewmodel&.set_weapon(@player_state.current_weapon_model) if @player_state.current_weapon != previous
|
|
2310
|
+
end
|
|
2311
|
+
|
|
2312
|
+
def fire_weapon
|
|
2313
|
+
return unless @player_state.alive?
|
|
2314
|
+
return if @game_time.to_f < @attack_finished.to_f
|
|
2315
|
+
|
|
2316
|
+
play_quad_attack_sound
|
|
2317
|
+
|
|
2318
|
+
if lightning_underwater_discharge?
|
|
2319
|
+
mark_player_show_hostile
|
|
2320
|
+
return if fire_lightning_underwater_discharge
|
|
2321
|
+
end
|
|
2322
|
+
|
|
2323
|
+
event = @player_state.fire_current_weapon(water_level: @player.water_level, deathmatch: deathmatch)
|
|
2324
|
+
return unless event
|
|
2325
|
+
|
|
2326
|
+
mark_player_show_hostile
|
|
2327
|
+
@attack_finished = @game_time + @player_state.current_weapon_cooldown
|
|
2328
|
+
trigger_weapon_punch(event)
|
|
2329
|
+
play_weapon_fire_sound(event)
|
|
2330
|
+
@viewmodel&.play_attack(viewmodel_attack_frames(event))
|
|
2331
|
+
add_muzzle_flash_dynamic_light unless event.kind == :axe
|
|
2332
|
+
case event.kind
|
|
2333
|
+
when :axe
|
|
2334
|
+
fire_axe(event)
|
|
2335
|
+
when :spike
|
|
2336
|
+
fire_spike(event)
|
|
2337
|
+
when :rocket
|
|
2338
|
+
fire_rocket(event)
|
|
2339
|
+
when :lightning
|
|
2340
|
+
fire_lightning(event)
|
|
2341
|
+
when :grenade
|
|
2342
|
+
fire_grenade(event)
|
|
2343
|
+
else
|
|
2344
|
+
fire_bullets(event)
|
|
2345
|
+
end
|
|
2346
|
+
end
|
|
2347
|
+
|
|
2348
|
+
def viewmodel_attack_frames(event)
|
|
2349
|
+
return event.animation_frames unless event.weapon == :axe
|
|
2350
|
+
|
|
2351
|
+
r = rand
|
|
2352
|
+
if r < 0.25
|
|
2353
|
+
1..4
|
|
2354
|
+
elsif r < 0.5
|
|
2355
|
+
5..8
|
|
2356
|
+
elsif r < 0.75
|
|
2357
|
+
1..4
|
|
2358
|
+
else
|
|
2359
|
+
5..8
|
|
2360
|
+
end
|
|
2361
|
+
end
|
|
2362
|
+
|
|
2363
|
+
def mark_player_show_hostile
|
|
2364
|
+
@player_state.show_hostile = @game_time.to_f + 1.0
|
|
2365
|
+
end
|
|
2366
|
+
|
|
2367
|
+
def play_quad_attack_sound
|
|
2368
|
+
return unless @player_state.powerup_active?(:quad, game_time: @game_time)
|
|
2369
|
+
return unless (@quad_attack_sound_finished || 0.0) < @game_time
|
|
2370
|
+
|
|
2371
|
+
@quad_attack_sound_finished = @game_time + 1.0
|
|
2372
|
+
@sound_events&.on_player(:quad_attack)
|
|
2373
|
+
end
|
|
2374
|
+
|
|
2375
|
+
def lightning_underwater_discharge?
|
|
2376
|
+
@player_state.current_weapon == :lightning_gun &&
|
|
2377
|
+
@player.water_level > 1 &&
|
|
2378
|
+
@player_state.can_fire_current_weapon?
|
|
2379
|
+
end
|
|
2380
|
+
|
|
2381
|
+
def fire_lightning_underwater_discharge
|
|
2382
|
+
if deathmatch.to_i > 3 && rand <= 0.5
|
|
2383
|
+
@player_state.deathtype = "selfwater"
|
|
2384
|
+
damage_player(4000.0, inflictor: @player)
|
|
2385
|
+
return false
|
|
2386
|
+
end
|
|
2387
|
+
|
|
2388
|
+
cells = @player_state.ammo[:cells]
|
|
2389
|
+
@player_state.ammo[:cells] = 0
|
|
2390
|
+
@attack_finished = @game_time + @player_state.current_weapon_cooldown
|
|
2391
|
+
play_lightning_start_sound
|
|
2392
|
+
@viewmodel&.play_attack(PlayerState::WEAPON_FIRE.dig(:lightning_gun, :animation_frames))
|
|
2393
|
+
apply_radius_damage(@player.position, 35.0 * cells, inflictor: @player)
|
|
2394
|
+
true
|
|
2395
|
+
end
|
|
2396
|
+
|
|
2397
|
+
def play_weapon_fire_sound(event)
|
|
2398
|
+
if event.kind == :lightning
|
|
2399
|
+
play_lightning_weapon_sounds
|
|
2400
|
+
else
|
|
2401
|
+
@sound_events&.on_weapon_fire(event)
|
|
2402
|
+
end
|
|
2403
|
+
end
|
|
2404
|
+
|
|
2405
|
+
def play_lightning_weapon_sounds
|
|
2406
|
+
play_lightning_start_sound
|
|
2407
|
+
|
|
2408
|
+
return unless (@lightning_hit_sound_finished || 0.0) < @game_time
|
|
2409
|
+
|
|
2410
|
+
@lightning_hit_sound_finished = @game_time + 0.6
|
|
2411
|
+
@sound_events&.on_weapon_fire(WeaponSound.new(sound: "weapons/lhit.wav"))
|
|
2412
|
+
end
|
|
2413
|
+
|
|
2414
|
+
def play_lightning_start_sound
|
|
2415
|
+
if (@lightning_attack_active_until || 0.0) < @game_time
|
|
2416
|
+
@sound_events&.on_weapon_fire(WeaponSound.new(sound: "weapons/lstart.wav"))
|
|
2417
|
+
end
|
|
2418
|
+
@lightning_attack_active_until = @game_time + 0.2
|
|
2419
|
+
end
|
|
2420
|
+
|
|
2421
|
+
def fire_axe(event)
|
|
2422
|
+
src = Math::Vec3.new(@player.position.x, @player.position.y, @player.position.z + 16.0)
|
|
2423
|
+
dir = @player.forward
|
|
2424
|
+
solid_brush = @entities.select(&:brush_entity?)
|
|
2425
|
+
trace = Physics::HullTrace.trace_world_and_entities(
|
|
2426
|
+
@level, src, src + dir * event.range, solid_brush, hull_num: 0
|
|
2427
|
+
)
|
|
2428
|
+
entity_hit = trace_axe_touch_entity(src, dir, event.range * trace.fraction)
|
|
2429
|
+
if entity_hit
|
|
2430
|
+
ent, hit_pos = entity_hit
|
|
2431
|
+
impact = hit_pos - dir * 4.0
|
|
2432
|
+
if ent.damageable?
|
|
2433
|
+
ent.instance_variable_set(:@axhitme, 1)
|
|
2434
|
+
damage = deathmatch.to_i > 3 ? 75 : event.damage
|
|
2435
|
+
damage_entity(ent, player_damage(damage), attacker: @player_state, inflictor: @player)
|
|
2436
|
+
@particles&.blood(impact, count: 20)
|
|
2437
|
+
else
|
|
2438
|
+
@sound_events&.on_player(:axe_wall_hit)
|
|
2439
|
+
@particles&.gunshot(impact, count: 3)
|
|
2440
|
+
end
|
|
2441
|
+
return
|
|
2442
|
+
end
|
|
2443
|
+
|
|
2444
|
+
return if trace.fraction >= 1.0
|
|
2445
|
+
|
|
2446
|
+
impact = trace.end_pos - dir * 4.0
|
|
2447
|
+
@sound_events&.on_player(:axe_wall_hit)
|
|
2448
|
+
@particles&.gunshot(impact, count: 3)
|
|
2449
|
+
end
|
|
2450
|
+
|
|
2451
|
+
def fire_spike(event)
|
|
2452
|
+
@projectiles ||= []
|
|
2453
|
+
offset = spike_lateral_offset(event.weapon)
|
|
2454
|
+
@projectiles << Projectile.new(
|
|
2455
|
+
kind: event.weapon == :super_nailgun ? :super_spike : :spike,
|
|
2456
|
+
position: @player.position + Math::Vec3.new(0.0, 0.0, 16.0) + @player.right * offset,
|
|
2457
|
+
velocity: @player.forward * 1000.0,
|
|
2458
|
+
damage: event.damage,
|
|
2459
|
+
damage_variance: event.damage_variance,
|
|
2460
|
+
radius_damage: event.radius_damage,
|
|
2461
|
+
explode_at: @game_time + SPIKE_LIFETIME,
|
|
2462
|
+
model: event.weapon == :super_nailgun ? "progs/s_spike.mdl" : "progs/spike.mdl",
|
|
2463
|
+
movetype: 9,
|
|
2464
|
+
solid: 2,
|
|
2465
|
+
effects: 0,
|
|
2466
|
+
angles: projectile_angles_from_velocity(@player.forward * 1000.0),
|
|
2467
|
+
avelocity: Math::Vec3::ORIGIN,
|
|
2468
|
+
voided: false,
|
|
2469
|
+
owner: @player_state
|
|
2470
|
+
)
|
|
2471
|
+
end
|
|
2472
|
+
|
|
2473
|
+
def fire_rocket(event)
|
|
2474
|
+
@projectiles ||= []
|
|
2475
|
+
@projectiles << Projectile.new(
|
|
2476
|
+
kind: :rocket,
|
|
2477
|
+
position: @player.position + @player.forward * 8.0 + Math::Vec3.new(0.0, 0.0, 16.0),
|
|
2478
|
+
velocity: @player.forward * 1000.0,
|
|
2479
|
+
damage: event.damage,
|
|
2480
|
+
damage_variance: event.damage_variance,
|
|
2481
|
+
radius_damage: event.radius_damage,
|
|
2482
|
+
explode_at: @game_time + ROCKET_LIFETIME,
|
|
2483
|
+
model: "progs/missile.mdl",
|
|
2484
|
+
movetype: 9,
|
|
2485
|
+
solid: 2,
|
|
2486
|
+
effects: 0,
|
|
2487
|
+
angles: projectile_angles_from_velocity(@player.forward * 1000.0),
|
|
2488
|
+
avelocity: Math::Vec3::ORIGIN,
|
|
2489
|
+
voided: false,
|
|
2490
|
+
owner: @player_state
|
|
2491
|
+
)
|
|
2492
|
+
end
|
|
2493
|
+
|
|
2494
|
+
def fire_lightning(event)
|
|
2495
|
+
return unless @level
|
|
2496
|
+
|
|
2497
|
+
src = @player.position + Math::Vec3.new(0.0, 0.0, 16.0)
|
|
2498
|
+
dir = @player.forward
|
|
2499
|
+
solid_brush = @entities.select(&:brush_entity?)
|
|
2500
|
+
trace = Physics::HullTrace.trace_world_and_entities(
|
|
2501
|
+
@level, src, src + dir * event.range, solid_brush, hull_num: 0
|
|
2502
|
+
)
|
|
2503
|
+
spawn_temp_lightning_beam(
|
|
2504
|
+
entity: VIEW_ENTITY,
|
|
2505
|
+
start: src,
|
|
2506
|
+
finish: trace.end_pos,
|
|
2507
|
+
model: "progs/bolt2.mdl"
|
|
2508
|
+
)
|
|
2509
|
+
lightning_damage(@player.position, trace.end_pos + dir * 4.0, event.damage)
|
|
2510
|
+
end
|
|
2511
|
+
|
|
2512
|
+
def spawn_temp_lightning_beam(entity:, start:, finish:, model:)
|
|
2513
|
+
@temporary_beams ||= []
|
|
2514
|
+
beam = TemporaryBeam.new(
|
|
2515
|
+
entity: entity,
|
|
2516
|
+
model: model,
|
|
2517
|
+
end_time: @game_time.to_f + 0.2,
|
|
2518
|
+
start: start,
|
|
2519
|
+
finish: finish
|
|
2520
|
+
)
|
|
2521
|
+
index = @temporary_beams.find_index { |existing| existing.entity == entity }
|
|
2522
|
+
index ||= @temporary_beams.find_index { |existing| existing.end_time < @game_time.to_f }
|
|
2523
|
+
if index
|
|
2524
|
+
@temporary_beams[index] = beam
|
|
2525
|
+
elsif @temporary_beams.size < MAX_BEAMS
|
|
2526
|
+
@temporary_beams << beam
|
|
2527
|
+
end
|
|
2528
|
+
beam
|
|
2529
|
+
end
|
|
2530
|
+
|
|
2531
|
+
def lightning_damage(src, dst, damage)
|
|
2532
|
+
dir = (dst - src).normalize
|
|
2533
|
+
side = quake_lightning_side_offset(dir)
|
|
2534
|
+
offsets = [
|
|
2535
|
+
Math::Vec3::ORIGIN,
|
|
2536
|
+
side,
|
|
2537
|
+
side * -1.0
|
|
2538
|
+
]
|
|
2539
|
+
trace_entities = []
|
|
2540
|
+
|
|
2541
|
+
offsets.each_with_index do |offset, index|
|
|
2542
|
+
hit = trace_lightning_damage(src + offset, dst + offset)
|
|
2543
|
+
next unless hit
|
|
2544
|
+
|
|
2545
|
+
ent, hit_pos = hit
|
|
2546
|
+
next if trace_entities.include?(ent)
|
|
2547
|
+
|
|
2548
|
+
trace_entities << ent
|
|
2549
|
+
next unless ent.damageable?
|
|
2550
|
+
|
|
2551
|
+
damage_entity(ent, player_damage(damage), attacker: @player_state, inflictor: @player)
|
|
2552
|
+
apply_direct_lightning_player_kick(ent) if index.zero?
|
|
2553
|
+
@particles&.lightning_blood(hit_pos)
|
|
2554
|
+
end
|
|
2555
|
+
end
|
|
2556
|
+
|
|
2557
|
+
def apply_direct_lightning_player_kick(ent)
|
|
2558
|
+
return unless ent.classname == "player"
|
|
2559
|
+
|
|
2560
|
+
ent.velocity = Math::Vec3.new(ent.velocity.x, ent.velocity.y, ent.velocity.z + 400.0)
|
|
2561
|
+
end
|
|
2562
|
+
|
|
2563
|
+
def quake_lightning_side_offset(dir)
|
|
2564
|
+
# QuakeC LightningDamage mutates f_x before assigning f_y, so both
|
|
2565
|
+
# horizontal side-offset components use the negated original y value.
|
|
2566
|
+
component = -dir.y
|
|
2567
|
+
Math::Vec3.new(component, component, 0.0) * 16.0
|
|
2568
|
+
end
|
|
2569
|
+
|
|
2570
|
+
def trace_lightning_damage(src, dst)
|
|
2571
|
+
dir = (dst - src).normalize
|
|
2572
|
+
solid_brush = @entities.select(&:brush_entity?)
|
|
2573
|
+
trace = Physics::HullTrace.trace_world_and_entities(
|
|
2574
|
+
@level, src, dst, solid_brush, hull_num: 0
|
|
2575
|
+
)
|
|
2576
|
+
trace_lightning_touch_entity(src, dir, (dst - src).length * trace.fraction)
|
|
2577
|
+
end
|
|
2578
|
+
|
|
2579
|
+
def fire_grenade(event)
|
|
2580
|
+
@projectiles ||= []
|
|
2581
|
+
velocity = if @player.pitch.zero?
|
|
2582
|
+
@player.forward_flat * 600.0 + Math::Vec3.new(0.0, 0.0, 200.0)
|
|
2583
|
+
else
|
|
2584
|
+
@player.forward * 600.0 + @player.up * 200.0 +
|
|
2585
|
+
@player.right * (crandom * 10.0) + @player.up * (crandom * 10.0)
|
|
2586
|
+
end
|
|
2587
|
+
|
|
2588
|
+
@projectiles << Projectile.new(
|
|
2589
|
+
kind: :grenade,
|
|
2590
|
+
position: @player.position,
|
|
2591
|
+
velocity: velocity,
|
|
2592
|
+
damage: event.damage,
|
|
2593
|
+
damage_variance: event.damage_variance,
|
|
2594
|
+
radius_damage: event.radius_damage,
|
|
2595
|
+
explode_at: @game_time + GRENADE_FUSE,
|
|
2596
|
+
model: "progs/grenade.mdl",
|
|
2597
|
+
movetype: 10,
|
|
2598
|
+
solid: 2,
|
|
2599
|
+
effects: 0,
|
|
2600
|
+
angles: projectile_angles_from_velocity(velocity),
|
|
2601
|
+
avelocity: Math::Vec3.new(300.0, 300.0, 300.0),
|
|
2602
|
+
voided: false,
|
|
2603
|
+
owner: @player_state
|
|
2604
|
+
)
|
|
2605
|
+
return unless deathmatch.to_i == 4
|
|
2606
|
+
|
|
2607
|
+
@attack_finished = @game_time.to_f + 1.1
|
|
2608
|
+
damage_player(10.0, inflictor: @player)
|
|
2609
|
+
end
|
|
2610
|
+
|
|
2611
|
+
def update_triggered_misc_entities
|
|
2612
|
+
@entities.each do |ent|
|
|
2613
|
+
case ent.classname
|
|
2614
|
+
when "trap_spikeshooter", "trap_shooter"
|
|
2615
|
+
if ent.instance_variable_get(:@triggered)
|
|
2616
|
+
ent.instance_variable_set(:@triggered, false)
|
|
2617
|
+
fire_spikeshooter(ent)
|
|
2618
|
+
end
|
|
2619
|
+
update_trap_shooter(ent) if ent.classname == "trap_shooter"
|
|
2620
|
+
when "fireball"
|
|
2621
|
+
update_fireball_emitter(ent) if ent.instance_variable_get(:@fireball_emitter)
|
|
2622
|
+
when "misc_noisemaker"
|
|
2623
|
+
update_noisemaker(ent)
|
|
2624
|
+
end
|
|
2625
|
+
end
|
|
2626
|
+
end
|
|
2627
|
+
|
|
2628
|
+
def setup_misc_entities
|
|
2629
|
+
@entities.each do |ent|
|
|
2630
|
+
case ent.classname
|
|
2631
|
+
when "trap_spikeshooter"
|
|
2632
|
+
setup_trap_spikeshooter(ent)
|
|
2633
|
+
when "trap_shooter"
|
|
2634
|
+
setup_trap_spikeshooter(ent)
|
|
2635
|
+
ent.wait = 1.0 if ent.wait.zero?
|
|
2636
|
+
ent.think_time = @game_time + ent["nextthink"].to_f + ent.wait
|
|
2637
|
+
ent.instance_variable_set(:@think, :shooter_think)
|
|
2638
|
+
when "misc_fireball"
|
|
2639
|
+
setup_fireball_emitter(ent)
|
|
2640
|
+
when "misc_noisemaker"
|
|
2641
|
+
setup_noisemaker(ent)
|
|
2642
|
+
when "misc_explobox", "misc_explobox2"
|
|
2643
|
+
setup_explobox(ent)
|
|
2644
|
+
when "viewthing"
|
|
2645
|
+
setup_viewthing(ent)
|
|
2646
|
+
when "light_globe", "light_torch_small_walltorch",
|
|
2647
|
+
"light_flame_large_yellow", "light_flame_small_yellow",
|
|
2648
|
+
"light_flame_small_white"
|
|
2649
|
+
setup_static_light_model(ent)
|
|
2650
|
+
when "item_key1", "item_key2"
|
|
2651
|
+
setup_key_item(ent)
|
|
2652
|
+
when "item_sigil"
|
|
2653
|
+
setup_sigil_item(ent)
|
|
2654
|
+
when "item_artifact_invulnerability", "item_artifact_envirosuit",
|
|
2655
|
+
"item_artifact_invisibility", "item_artifact_super_damage"
|
|
2656
|
+
setup_powerup_item(ent)
|
|
2657
|
+
when "item_health"
|
|
2658
|
+
setup_health_item(ent)
|
|
2659
|
+
when "item_armor1", "item_armor2", "item_armorInv"
|
|
2660
|
+
setup_armor_item(ent)
|
|
2661
|
+
when "item_shells", "item_spikes", "item_rockets", "item_cells", "item_weapon"
|
|
2662
|
+
setup_ammo_item(ent)
|
|
2663
|
+
when "weapon_supershotgun", "weapon_nailgun", "weapon_supernailgun",
|
|
2664
|
+
"weapon_grenadelauncher", "weapon_rocketlauncher", "weapon_lightning"
|
|
2665
|
+
setup_weapon_item(ent)
|
|
2666
|
+
when "info_null", "air_bubbles", "noclass", "event_lightning",
|
|
2667
|
+
*QUAKEWORLD_REMOVED_MONSTER_CLASSNAMES
|
|
2668
|
+
ent.instance_variable_set(:@removed, true)
|
|
2669
|
+
when "light"
|
|
2670
|
+
if ent.targetname
|
|
2671
|
+
setup_toggle_light(ent)
|
|
2672
|
+
else
|
|
2673
|
+
ent.instance_variable_set(:@removed, true)
|
|
2674
|
+
end
|
|
2675
|
+
when "light_fluoro"
|
|
2676
|
+
setup_toggle_light(ent)
|
|
2677
|
+
when "light_fluorospark"
|
|
2678
|
+
setup_fluorospark_light(ent)
|
|
2679
|
+
when "info_intermission"
|
|
2680
|
+
setup_intermission(ent)
|
|
2681
|
+
when "info_teleport_destination"
|
|
2682
|
+
setup_teleport_destination(ent)
|
|
2683
|
+
when "path_corner"
|
|
2684
|
+
setup_path_corner(ent)
|
|
2685
|
+
end
|
|
2686
|
+
|
|
2687
|
+
setup_start_item(ent) if start_item?(ent) && !ent.removed?
|
|
2688
|
+
end
|
|
2689
|
+
end
|
|
2690
|
+
|
|
2691
|
+
def start_item?(ent)
|
|
2692
|
+
START_ITEM_CLASSNAMES.include?(ent.classname)
|
|
2693
|
+
end
|
|
2694
|
+
|
|
2695
|
+
def setup_start_item(ent)
|
|
2696
|
+
mins, maxs = start_item_bounds(ent.classname)
|
|
2697
|
+
ent.instance_variable_set(:@mins, mins)
|
|
2698
|
+
ent.instance_variable_set(:@maxs, maxs)
|
|
2699
|
+
ent.instance_variable_set(:@solid, 0)
|
|
2700
|
+
ent.think_time = (@game_time || 0.0) + 0.2
|
|
2701
|
+
ent.instance_variable_set(:@think, :place_item)
|
|
2702
|
+
end
|
|
2703
|
+
|
|
2704
|
+
def start_item_bounds(classname)
|
|
2705
|
+
if ITEM_WIDE_BOUNDS_CLASSNAMES.include?(classname)
|
|
2706
|
+
[Math::Vec3::ORIGIN, Math::Vec3.new(32.0, 32.0, 56.0)]
|
|
2707
|
+
elsif ITEM_TALL_BOUNDS_CLASSNAMES.include?(classname)
|
|
2708
|
+
[Math::Vec3.new(-16.0, -16.0, 0.0), Math::Vec3.new(16.0, 16.0, 56.0)]
|
|
2709
|
+
else
|
|
2710
|
+
[Math::Vec3.new(-16.0, -16.0, -24.0), Math::Vec3.new(16.0, 16.0, 32.0)]
|
|
2711
|
+
end
|
|
2712
|
+
end
|
|
2713
|
+
|
|
2714
|
+
def update_start_items
|
|
2715
|
+
return unless @entities
|
|
2716
|
+
|
|
2717
|
+
@entities.each do |ent|
|
|
2718
|
+
next unless ent.instance_variable_get(:@think) == :place_item
|
|
2719
|
+
next if ent.think_time > @game_time
|
|
2720
|
+
|
|
2721
|
+
place_item(ent)
|
|
2722
|
+
end
|
|
2723
|
+
end
|
|
2724
|
+
|
|
2725
|
+
def place_item(ent)
|
|
2726
|
+
ent.instance_variable_set(:@mdl, ent["model"])
|
|
2727
|
+
ent.flags = 256
|
|
2728
|
+
ent.instance_variable_set(:@solid, 1)
|
|
2729
|
+
ent.instance_variable_set(:@movetype, 6)
|
|
2730
|
+
ent.velocity = Math::Vec3::ORIGIN
|
|
2731
|
+
ent.position += Math::Vec3.new(0.0, 0.0, 6.0)
|
|
2732
|
+
return unless drop_item_to_floor(ent)
|
|
2733
|
+
|
|
2734
|
+
ent.think_time = 0.0
|
|
2735
|
+
ent.instance_variable_set(:@think, nil)
|
|
2736
|
+
end
|
|
2737
|
+
|
|
2738
|
+
def drop_item_to_floor(ent)
|
|
2739
|
+
return true unless @level
|
|
2740
|
+
|
|
2741
|
+
floor = trace_entity_floor(ent)
|
|
2742
|
+
unless floor
|
|
2743
|
+
ent.instance_variable_set(:@removed, true)
|
|
2744
|
+
return false
|
|
2745
|
+
end
|
|
2746
|
+
|
|
2747
|
+
ent.position = floor
|
|
2748
|
+
ent.flags |= FL_ONGROUND
|
|
2749
|
+
true
|
|
2750
|
+
end
|
|
2751
|
+
|
|
2752
|
+
HULL1_CLIP_MINS = Math::Vec3.new(-16.0, -16.0, -24.0)
|
|
2753
|
+
|
|
2754
|
+
def trace_entity_floor(ent)
|
|
2755
|
+
# SV_ClipMoveToEntity aligns the moving entity's box with the
|
|
2756
|
+
# clipping hull: offset = hull.clip_mins - ent.mins. Items have
|
|
2757
|
+
# mins.z = 0, so without this the player-hull trace starts 24
|
|
2758
|
+
# units inside the floor and reports all_solid ("bonus item fell
|
|
2759
|
+
# out of level" removes the item).
|
|
2760
|
+
ent_mins = ent.instance_variable_get(:@mins)
|
|
2761
|
+
offset = ent_mins ? HULL1_CLIP_MINS - ent_mins : Math::Vec3::ORIGIN
|
|
2762
|
+
start_pos = ent.position - offset
|
|
2763
|
+
end_pos = start_pos + Math::Vec3.new(0.0, 0.0, -256.0)
|
|
2764
|
+
trace = Physics::HullTrace.trace_world_and_entities(@level, start_pos, end_pos, @entities)
|
|
2765
|
+
return nil if trace.fraction == 1.0 || trace.all_solid
|
|
2766
|
+
|
|
2767
|
+
trace.end_pos + offset
|
|
2768
|
+
end
|
|
2769
|
+
|
|
2770
|
+
def setup_key_item(ent)
|
|
2771
|
+
worldtype = current_worldtype
|
|
2772
|
+
key = key_spawn_fields(ent.classname, worldtype)
|
|
2773
|
+
ent.properties["model"] = key[:model]
|
|
2774
|
+
ent.instance_variable_set(:@touch, :key_touch)
|
|
2775
|
+
ent.instance_variable_set(:@noise, key_sound(worldtype))
|
|
2776
|
+
ent.instance_variable_set(:@netname, key[:netname])
|
|
2777
|
+
ent.instance_variable_set(:@items, key[:items])
|
|
2778
|
+
end
|
|
2779
|
+
|
|
2780
|
+
def setup_sigil_item(ent)
|
|
2781
|
+
raise "no spawnflags" if ent.spawnflags.zero?
|
|
2782
|
+
|
|
2783
|
+
ent.properties["model"] = sigil_model(ent.spawnflags)
|
|
2784
|
+
ent.instance_variable_set(:@touch, :sigil_touch)
|
|
2785
|
+
ent.instance_variable_set(:@noise, "misc/runekey.wav")
|
|
2786
|
+
end
|
|
2787
|
+
|
|
2788
|
+
def setup_powerup_item(ent)
|
|
2789
|
+
powerup = powerup_spawn_fields(ent.classname)
|
|
2790
|
+
ent.properties["model"] = powerup[:model]
|
|
2791
|
+
ent.instance_variable_set(:@touch, :powerup_touch)
|
|
2792
|
+
ent.instance_variable_set(:@noise, powerup[:noise])
|
|
2793
|
+
ent.instance_variable_set(:@netname, powerup[:netname])
|
|
2794
|
+
ent.instance_variable_set(:@items, powerup[:items])
|
|
2795
|
+
ent.instance_variable_set(:@effects, powerup_effects(ent, powerup[:effects])) if powerup[:effects]
|
|
2796
|
+
ent.instance_variable_set(:@mins, ENTITY_HULL_MINS)
|
|
2797
|
+
ent.instance_variable_set(:@maxs, ENTITY_HULL_MAXS)
|
|
2798
|
+
end
|
|
2799
|
+
|
|
2800
|
+
def powerup_effects(ent, effects)
|
|
2801
|
+
existing = ent.instance_variable_get(:@effects)
|
|
2802
|
+
existing ||= ent["effects"].to_i if ent["effects"]
|
|
2803
|
+
existing.to_i | effects
|
|
2804
|
+
end
|
|
2805
|
+
|
|
2806
|
+
def key_spawn_fields(classname, worldtype)
|
|
2807
|
+
case [classname, worldtype]
|
|
2808
|
+
when ["item_key1", 1]
|
|
2809
|
+
{ model: "progs/m_s_key.mdl", netname: "silver runekey", items: 131_072 }
|
|
2810
|
+
when ["item_key1", 2]
|
|
2811
|
+
{ model: "progs/b_s_key.mdl", netname: "silver keycard", items: 131_072 }
|
|
2812
|
+
when ["item_key2", 1]
|
|
2813
|
+
{ model: "progs/m_g_key.mdl", netname: "gold runekey", items: 262_144 }
|
|
2814
|
+
when ["item_key2", 2]
|
|
2815
|
+
{ model: "progs/b_g_key.mdl", netname: "gold keycard", items: 262_144 }
|
|
2816
|
+
else
|
|
2817
|
+
medieval_key_spawn_fields(classname)
|
|
2818
|
+
end
|
|
2819
|
+
end
|
|
2820
|
+
|
|
2821
|
+
def medieval_key_spawn_fields(classname)
|
|
2822
|
+
if classname == "item_key2"
|
|
2823
|
+
{ model: "progs/w_g_key.mdl", netname: "gold key", items: 262_144 }
|
|
2824
|
+
else
|
|
2825
|
+
{ model: "progs/w_s_key.mdl", netname: "silver key", items: 131_072 }
|
|
2826
|
+
end
|
|
2827
|
+
end
|
|
2828
|
+
|
|
2829
|
+
def powerup_spawn_fields(classname)
|
|
2830
|
+
case classname
|
|
2831
|
+
when "item_artifact_envirosuit"
|
|
2832
|
+
{ model: "progs/suit.mdl", noise: "items/suit.wav", netname: "Biosuit", items: 2_097_152 }
|
|
2833
|
+
when "item_artifact_invulnerability"
|
|
2834
|
+
{
|
|
2835
|
+
model: "progs/invulner.mdl",
|
|
2836
|
+
noise: "items/protect.wav",
|
|
2837
|
+
netname: "Pentagram of Protection",
|
|
2838
|
+
items: 1_048_576,
|
|
2839
|
+
effects: 128
|
|
2840
|
+
}
|
|
2841
|
+
when "item_artifact_invisibility"
|
|
2842
|
+
{ model: "progs/invisibl.mdl", noise: "items/inv1.wav", netname: "Ring of Shadows", items: 524_288 }
|
|
2843
|
+
when "item_artifact_super_damage"
|
|
2844
|
+
{
|
|
2845
|
+
model: "progs/quaddama.mdl",
|
|
2846
|
+
noise: "items/damage.wav",
|
|
2847
|
+
netname: deathmatch.to_i == 4 ? "OctaPower" : "Quad Damage",
|
|
2848
|
+
items: 4_194_304,
|
|
2849
|
+
effects: 64
|
|
2850
|
+
}
|
|
2851
|
+
end
|
|
2852
|
+
end
|
|
2853
|
+
|
|
2854
|
+
def sigil_model(spawnflags)
|
|
2855
|
+
if (spawnflags & 8) != 0
|
|
2856
|
+
"progs/end4.mdl"
|
|
2857
|
+
elsif (spawnflags & 4) != 0
|
|
2858
|
+
"progs/end3.mdl"
|
|
2859
|
+
elsif (spawnflags & 2) != 0
|
|
2860
|
+
"progs/end2.mdl"
|
|
2861
|
+
elsif (spawnflags & 1) != 0
|
|
2862
|
+
"progs/end1.mdl"
|
|
2863
|
+
end
|
|
2864
|
+
end
|
|
2865
|
+
|
|
2866
|
+
def setup_health_item(ent)
|
|
2867
|
+
health = health_spawn_fields(ent)
|
|
2868
|
+
ent.properties["model"] = health[:model]
|
|
2869
|
+
ent.instance_variable_set(:@touch, :health_touch)
|
|
2870
|
+
ent.instance_variable_set(:@noise, health[:noise])
|
|
2871
|
+
ent.instance_variable_set(:@healamount, health[:healamount])
|
|
2872
|
+
ent.instance_variable_set(:@healtype, health[:healtype])
|
|
2873
|
+
end
|
|
2874
|
+
|
|
2875
|
+
def setup_armor_item(ent)
|
|
2876
|
+
armor = armor_spawn_fields(ent.classname)
|
|
2877
|
+
ent.properties["model"] = armor[:model]
|
|
2878
|
+
ent.skin = armor[:skin]
|
|
2879
|
+
ent.instance_variable_set(:@touch, :armor_touch)
|
|
2880
|
+
end
|
|
2881
|
+
|
|
2882
|
+
def armor_spawn_fields(classname)
|
|
2883
|
+
skin = case classname
|
|
2884
|
+
when "item_armor2" then 1
|
|
2885
|
+
when "item_armorInv" then 2
|
|
2886
|
+
else 0
|
|
2887
|
+
end
|
|
2888
|
+
{ model: "progs/armor.mdl", skin: skin }
|
|
2889
|
+
end
|
|
2890
|
+
|
|
2891
|
+
def health_spawn_fields(ent)
|
|
2892
|
+
if (ent.spawnflags & 1) != 0
|
|
2893
|
+
{ model: "maps/b_bh10.bsp", noise: "items/r_item1.wav", healamount: 15, healtype: 0 }
|
|
2894
|
+
elsif (ent.spawnflags & 2) != 0
|
|
2895
|
+
{ model: "maps/b_bh100.bsp", noise: "items/r_item2.wav", healamount: 100, healtype: 2 }
|
|
2896
|
+
else
|
|
2897
|
+
{ model: "maps/b_bh25.bsp", noise: "items/health1.wav", healamount: 25, healtype: 1 }
|
|
2898
|
+
end
|
|
2899
|
+
end
|
|
2900
|
+
|
|
2901
|
+
def setup_ammo_item(ent)
|
|
2902
|
+
if deathmatch.to_i == 4 && ent.classname != "item_weapon"
|
|
2903
|
+
disable_spawned_item(ent)
|
|
2904
|
+
return
|
|
2905
|
+
end
|
|
2906
|
+
|
|
2907
|
+
ammo = ammo_spawn_fields(ent)
|
|
2908
|
+
if ammo.nil? && ent.classname == "item_weapon"
|
|
2909
|
+
ent.instance_variable_set(:@touch, :ammo_touch)
|
|
2910
|
+
return
|
|
2911
|
+
end
|
|
2912
|
+
return unless ammo
|
|
2913
|
+
|
|
2914
|
+
ent.properties["model"] = ammo[:model]
|
|
2915
|
+
ent.instance_variable_set(:@touch, :ammo_touch)
|
|
2916
|
+
ent.instance_variable_set(:@aflag, ammo[:amount])
|
|
2917
|
+
ent.instance_variable_set(:@weapon, ammo[:weapon])
|
|
2918
|
+
ent.instance_variable_set(:@netname, ammo[:netname])
|
|
2919
|
+
end
|
|
2920
|
+
|
|
2921
|
+
def ammo_spawn_fields(ent)
|
|
2922
|
+
big = (ent.spawnflags & 1) != 0
|
|
2923
|
+
case ent.classname
|
|
2924
|
+
when "item_shells"
|
|
2925
|
+
{ model: big ? "maps/b_shell1.bsp" : "maps/b_shell0.bsp", weapon: 1, netname: "shells", amount: big ? 40 : 20 }
|
|
2926
|
+
when "item_spikes"
|
|
2927
|
+
{ model: big ? "maps/b_nail1.bsp" : "maps/b_nail0.bsp", weapon: 2, netname: "nails", amount: big ? 50 : 25 }
|
|
2928
|
+
when "item_rockets"
|
|
2929
|
+
{ model: big ? "maps/b_rock1.bsp" : "maps/b_rock0.bsp", weapon: 3, netname: "rockets", amount: big ? 10 : 5 }
|
|
2930
|
+
when "item_cells"
|
|
2931
|
+
{ model: big ? "maps/b_batt1.bsp" : "maps/b_batt0.bsp", weapon: 4, netname: "cells", amount: big ? 12 : 6 }
|
|
2932
|
+
when "item_weapon"
|
|
2933
|
+
legacy_weapon_spawn_fields(ent)
|
|
2934
|
+
end
|
|
2935
|
+
end
|
|
2936
|
+
|
|
2937
|
+
def legacy_weapon_spawn_fields(ent)
|
|
2938
|
+
flags = ent.spawnflags
|
|
2939
|
+
big = (flags & 8) != 0
|
|
2940
|
+
if (flags & 2) != 0
|
|
2941
|
+
{ model: big ? "maps/b_rock1.bsp" : "maps/b_rock0.bsp", weapon: 3, netname: "rockets", amount: big ? 10 : 5 }
|
|
2942
|
+
elsif (flags & 4) != 0
|
|
2943
|
+
{ model: big ? "maps/b_nail1.bsp" : "maps/b_nail0.bsp", weapon: 2, netname: "spikes", amount: big ? 40 : 20 }
|
|
2944
|
+
elsif (flags & 1) != 0
|
|
2945
|
+
{ model: big ? "maps/b_shell1.bsp" : "maps/b_shell0.bsp", weapon: 1, netname: "shells", amount: big ? 40 : 20 }
|
|
2946
|
+
end
|
|
2947
|
+
end
|
|
2948
|
+
|
|
2949
|
+
def setup_weapon_item(ent)
|
|
2950
|
+
if deathmatch.to_i > 3
|
|
2951
|
+
disable_spawned_item(ent)
|
|
2952
|
+
return
|
|
2953
|
+
end
|
|
2954
|
+
|
|
2955
|
+
weapon = weapon_spawn_fields(ent.classname)
|
|
2956
|
+
return unless weapon
|
|
2957
|
+
|
|
2958
|
+
ent.properties["model"] = weapon[:model]
|
|
2959
|
+
ent.instance_variable_set(:@touch, :weapon_touch)
|
|
2960
|
+
ent.instance_variable_set(:@weapon, weapon[:weapon])
|
|
2961
|
+
ent.instance_variable_set(:@netname, weapon[:netname])
|
|
2962
|
+
end
|
|
2963
|
+
|
|
2964
|
+
def disable_spawned_item(ent)
|
|
2965
|
+
ent.properties["model"] = ""
|
|
2966
|
+
ent.instance_variable_set(:@solid, 0)
|
|
2967
|
+
ent.instance_variable_set(:@removed, true)
|
|
2968
|
+
end
|
|
2969
|
+
|
|
2970
|
+
def weapon_spawn_fields(classname)
|
|
2971
|
+
case classname
|
|
2972
|
+
when "weapon_supershotgun"
|
|
2973
|
+
{ model: "progs/g_shot.mdl", weapon: 2, netname: "Double-barrelled Shotgun" }
|
|
2974
|
+
when "weapon_nailgun"
|
|
2975
|
+
{ model: "progs/g_nail.mdl", weapon: 4, netname: "nailgun" }
|
|
2976
|
+
when "weapon_supernailgun"
|
|
2977
|
+
{ model: "progs/g_nail2.mdl", weapon: 8, netname: "Super Nailgun" }
|
|
2978
|
+
when "weapon_grenadelauncher"
|
|
2979
|
+
{ model: "progs/g_rock.mdl", weapon: 3, netname: "Grenade Launcher" }
|
|
2980
|
+
when "weapon_rocketlauncher"
|
|
2981
|
+
{ model: "progs/g_rock2.mdl", weapon: 3, netname: "Rocket Launcher" }
|
|
2982
|
+
when "weapon_lightning"
|
|
2983
|
+
{ model: "progs/g_light.mdl", weapon: 3, netname: "Thunderbolt" }
|
|
2984
|
+
end
|
|
2985
|
+
end
|
|
2986
|
+
|
|
2987
|
+
def setup_explobox(ent)
|
|
2988
|
+
height = ent.classname == "misc_explobox2" ? 32.0 : 64.0
|
|
2989
|
+
ent.properties["model"] = ent.classname == "misc_explobox2" ? "maps/b_exbox2.bsp" : "maps/b_explob.bsp"
|
|
2990
|
+
ent.health = 20.0
|
|
2991
|
+
ent.instance_variable_set(:@solid, 2)
|
|
2992
|
+
ent.instance_variable_set(:@movetype, 0)
|
|
2993
|
+
ent.instance_variable_set(:@takedamage, 2)
|
|
2994
|
+
ent.instance_variable_set(:@th_die, :barrel_explode)
|
|
2995
|
+
ent.instance_variable_set(:@mins, Math::Vec3::ORIGIN)
|
|
2996
|
+
ent.instance_variable_set(:@maxs, Math::Vec3.new(32.0, 32.0, height))
|
|
2997
|
+
unless ent.instance_variable_get(:@explobox_setup)
|
|
2998
|
+
ent.position += Math::Vec3.new(0.0, 0.0, 2.0)
|
|
2999
|
+
drop_explobox_to_floor(ent)
|
|
3000
|
+
ent.instance_variable_set(:@explobox_setup, true)
|
|
3001
|
+
end
|
|
3002
|
+
end
|
|
3003
|
+
|
|
3004
|
+
def drop_explobox_to_floor(ent)
|
|
3005
|
+
return unless @level
|
|
3006
|
+
|
|
3007
|
+
old_z = ent.position.z
|
|
3008
|
+
if (floor = trace_entity_floor(ent))
|
|
3009
|
+
ent.position = floor
|
|
3010
|
+
ent.flags |= FL_ONGROUND
|
|
3011
|
+
end
|
|
3012
|
+
ent.instance_variable_set(:@removed, true) if old_z - ent.position.z > 250.0
|
|
3013
|
+
end
|
|
3014
|
+
|
|
3015
|
+
def setup_viewthing(ent)
|
|
3016
|
+
ent.properties["model"] = "progs/player.mdl"
|
|
3017
|
+
ent.instance_variable_set(:@movetype, 0)
|
|
3018
|
+
ent.instance_variable_set(:@solid, 0)
|
|
3019
|
+
end
|
|
3020
|
+
|
|
3021
|
+
def setup_static_light_model(ent)
|
|
3022
|
+
light = static_light_spawn_fields(ent.classname)
|
|
3023
|
+
ent.properties["model"] = light[:model]
|
|
3024
|
+
ent.frame = light.fetch(:frame, 0)
|
|
3025
|
+
ent.instance_variable_set(:@static, true)
|
|
3026
|
+
end
|
|
3027
|
+
|
|
3028
|
+
def static_light_spawn_fields(classname)
|
|
3029
|
+
case classname
|
|
3030
|
+
when "light_globe"
|
|
3031
|
+
{ model: "progs/s_light.spr" }
|
|
3032
|
+
when "light_torch_small_walltorch"
|
|
3033
|
+
{ model: "progs/flame.mdl" }
|
|
3034
|
+
when "light_flame_large_yellow"
|
|
3035
|
+
{ model: "progs/flame2.mdl", frame: 1 }
|
|
3036
|
+
when "light_flame_small_yellow", "light_flame_small_white"
|
|
3037
|
+
{ model: "progs/flame2.mdl" }
|
|
3038
|
+
end
|
|
3039
|
+
end
|
|
3040
|
+
|
|
3041
|
+
def current_worldtype
|
|
3042
|
+
world = @entities.find { |ent| ent.classname == "worldspawn" }
|
|
3043
|
+
(world&.[]("worldtype") || "0").to_i
|
|
3044
|
+
end
|
|
3045
|
+
|
|
3046
|
+
def world_info_value(key)
|
|
3047
|
+
world = @entities&.find { |ent| ent.classname == "worldspawn" }
|
|
3048
|
+
world&.[](key)
|
|
3049
|
+
end
|
|
3050
|
+
|
|
3051
|
+
def configure_world_gravity(map_name)
|
|
3052
|
+
@gravity = map_name == "maps/e1m8.bsp" ? 100.0 : GRAVITY
|
|
3053
|
+
end
|
|
3054
|
+
|
|
3055
|
+
def current_gravity
|
|
3056
|
+
(@gravity || GRAVITY).to_f
|
|
3057
|
+
end
|
|
3058
|
+
|
|
3059
|
+
def world_info_enabled?(key)
|
|
3060
|
+
value = world_info_value(key)
|
|
3061
|
+
value && value.to_f != 0.0
|
|
3062
|
+
end
|
|
3063
|
+
|
|
3064
|
+
def key_sound(worldtype)
|
|
3065
|
+
case worldtype
|
|
3066
|
+
when 1
|
|
3067
|
+
"misc/runekey.wav"
|
|
3068
|
+
when 2
|
|
3069
|
+
"misc/basekey.wav"
|
|
3070
|
+
else
|
|
3071
|
+
"misc/medkey.wav"
|
|
3072
|
+
end
|
|
3073
|
+
end
|
|
3074
|
+
|
|
3075
|
+
def setup_toggle_light(ent)
|
|
3076
|
+
return if ent.style < 32
|
|
3077
|
+
|
|
3078
|
+
ent.instance_variable_set(:@use, :light_use)
|
|
3079
|
+
ent.lightstyle = (ent.spawnflags & 1) != 0 ? "a" : "m"
|
|
3080
|
+
end
|
|
3081
|
+
|
|
3082
|
+
def setup_fluorospark_light(ent)
|
|
3083
|
+
ent.style = 10 if ent.style.zero?
|
|
3084
|
+
end
|
|
3085
|
+
|
|
3086
|
+
def current_lightstyles
|
|
3087
|
+
lightstyles = Renderer::GLLightmap.default_lightstyles
|
|
3088
|
+
@entities&.each do |ent|
|
|
3089
|
+
next unless ent.lightstyle
|
|
3090
|
+
next if ent.style.negative? || ent.style >= Renderer::GLLightmap::MAX_LIGHTSTYLES
|
|
3091
|
+
|
|
3092
|
+
lightstyles[ent.style] = ent.lightstyle
|
|
3093
|
+
end
|
|
3094
|
+
lightstyles
|
|
3095
|
+
end
|
|
3096
|
+
|
|
3097
|
+
def setup_noisemaker(ent)
|
|
3098
|
+
ent.think_time = @game_time + 0.1 + rand
|
|
3099
|
+
ent.instance_variable_set(:@think, :noise_think)
|
|
3100
|
+
end
|
|
3101
|
+
|
|
3102
|
+
def update_noisemaker(ent)
|
|
3103
|
+
return if ent.think_time > @game_time
|
|
3104
|
+
|
|
3105
|
+
@sound_events&.on_misc_noisemaker(ent)
|
|
3106
|
+
ent.think_time = @game_time + 0.5
|
|
3107
|
+
end
|
|
3108
|
+
|
|
3109
|
+
def setup_intermission(ent)
|
|
3110
|
+
ent.angles = vec3_property(ent, "mangle")
|
|
3111
|
+
end
|
|
3112
|
+
|
|
3113
|
+
def find_intermission_spot
|
|
3114
|
+
intermissions = (@entities || []).select { |ent| ent.classname == "info_intermission" }
|
|
3115
|
+
unless intermissions.empty?
|
|
3116
|
+
spot = intermissions.first
|
|
3117
|
+
cyc = rand * 4.0
|
|
3118
|
+
while cyc > 1.0
|
|
3119
|
+
index = intermissions.index(spot) + 1
|
|
3120
|
+
spot = intermissions[index] || intermissions.first
|
|
3121
|
+
cyc -= 1.0
|
|
3122
|
+
end
|
|
3123
|
+
return spot
|
|
3124
|
+
end
|
|
3125
|
+
|
|
3126
|
+
start = (@entities || []).find { |ent| ent.classname == "info_player_start" }
|
|
3127
|
+
return start if start
|
|
3128
|
+
|
|
3129
|
+
raise "FindIntermission: no spot"
|
|
3130
|
+
end
|
|
3131
|
+
|
|
3132
|
+
def setup_teleport_destination(ent)
|
|
3133
|
+
return if ent.instance_variable_get(:@teleport_destination_setup)
|
|
3134
|
+
|
|
3135
|
+
ent.instance_variable_set(:@mangle, ent.angles)
|
|
3136
|
+
ent.angles = Math::Vec3::ORIGIN
|
|
3137
|
+
ent.properties["model"] = ""
|
|
3138
|
+
ent.position += Math::Vec3.new(0.0, 0.0, 27.0)
|
|
3139
|
+
raise "no targetname" if ent.targetname.nil? || ent.targetname.empty?
|
|
3140
|
+
|
|
3141
|
+
ent.instance_variable_set(:@teleport_destination_setup, true)
|
|
3142
|
+
end
|
|
3143
|
+
|
|
3144
|
+
def setup_path_corner(ent)
|
|
3145
|
+
raise "monster_movetarget: no targetname" if ent.targetname.nil? || ent.targetname.empty?
|
|
3146
|
+
|
|
3147
|
+
ent.instance_variable_set(:@solid, 1)
|
|
3148
|
+
ent.instance_variable_set(:@touch, :t_movetarget)
|
|
3149
|
+
ent.instance_variable_set(:@mins, Math::Vec3.new(-8.0, -8.0, -8.0))
|
|
3150
|
+
ent.instance_variable_set(:@maxs, Math::Vec3.new(8.0, 8.0, 8.0))
|
|
3151
|
+
end
|
|
3152
|
+
|
|
3153
|
+
def setup_ambient_sounds
|
|
3154
|
+
@entities.each { |ent| @sound_events&.on_ambient_entity(ent) }
|
|
3155
|
+
end
|
|
3156
|
+
|
|
3157
|
+
def update_trap_shooter(ent)
|
|
3158
|
+
return if ent.think_time > @game_time
|
|
3159
|
+
|
|
3160
|
+
fire_spikeshooter(ent)
|
|
3161
|
+
@projectiles[-1] = trap_shooter_projectile_with_quake_think_velocity(@projectiles[-1], ent)
|
|
3162
|
+
ent.think_time = @game_time + ent.wait
|
|
3163
|
+
ent.instance_variable_set(:@think, :shooter_think)
|
|
3164
|
+
end
|
|
3165
|
+
|
|
3166
|
+
def setup_trap_spikeshooter(ent)
|
|
3167
|
+
ent.move_dir = ent.forward_vector
|
|
3168
|
+
ent.angles = Math::Vec3::ORIGIN
|
|
3169
|
+
ent.instance_variable_set(:@use, :spikeshooter_use)
|
|
3170
|
+
end
|
|
3171
|
+
|
|
3172
|
+
def fire_spikeshooter(ent)
|
|
3173
|
+
@projectiles ||= []
|
|
3174
|
+
laser = (ent.spawnflags & 2) != 0
|
|
3175
|
+
superspike = (ent.spawnflags & 1) != 0
|
|
3176
|
+
move_dir = ent.move_dir == Math::Vec3::ORIGIN ? ent.forward_vector : ent.move_dir
|
|
3177
|
+
@sound_events&.on_weapon_fire(WeaponSound.new(sound: laser ? "enforcer/enfire.wav" : "weapons/spike2.wav"))
|
|
3178
|
+
@projectiles << Projectile.new(
|
|
3179
|
+
kind: laser ? :laser : (superspike ? :super_spike : :spike),
|
|
3180
|
+
position: ent.position,
|
|
3181
|
+
velocity: move_dir * (laser ? 600.0 : 500.0),
|
|
3182
|
+
damage: laser ? 15 : (superspike ? 18 : 9),
|
|
3183
|
+
damage_variance: nil,
|
|
3184
|
+
radius_damage: nil,
|
|
3185
|
+
explode_at: @game_time + (laser ? LASER_LIFETIME : SPIKE_LIFETIME),
|
|
3186
|
+
model: laser ? "progs/laser.mdl" : "progs/spike.mdl",
|
|
3187
|
+
movetype: laser ? 5 : 9,
|
|
3188
|
+
solid: 2,
|
|
3189
|
+
effects: laser ? 8 : 0,
|
|
3190
|
+
angles: projectile_angles_from_velocity(move_dir),
|
|
3191
|
+
avelocity: Math::Vec3::ORIGIN,
|
|
3192
|
+
voided: false,
|
|
3193
|
+
owner: ent
|
|
3194
|
+
)
|
|
3195
|
+
end
|
|
3196
|
+
|
|
3197
|
+
def trap_shooter_projectile_with_quake_think_velocity(projectile, ent)
|
|
3198
|
+
return projectile unless projectile
|
|
3199
|
+
|
|
3200
|
+
projectile.with(velocity: ent.move_dir * 500.0, owner: ent)
|
|
3201
|
+
end
|
|
3202
|
+
|
|
3203
|
+
def setup_fireball_emitter(ent)
|
|
3204
|
+
ent.classname = "fireball"
|
|
3205
|
+
ent.speed = 0.0 unless ent["speed"]
|
|
3206
|
+
ent.think_time = @game_time + rand * 5.0
|
|
3207
|
+
ent.instance_variable_set(:@think, :fire_fly)
|
|
3208
|
+
ent.instance_variable_set(:@fireball_emitter, true)
|
|
3209
|
+
end
|
|
3210
|
+
|
|
3211
|
+
def update_fireball_emitter(ent)
|
|
3212
|
+
return if ent.think_time > @game_time
|
|
3213
|
+
|
|
3214
|
+
fire_lava_ball(ent)
|
|
3215
|
+
ent.think_time = @game_time + rand * 5.0 + 3.0
|
|
3216
|
+
ent.instance_variable_set(:@think, :fire_fly)
|
|
3217
|
+
end
|
|
3218
|
+
|
|
3219
|
+
def fire_lava_ball(ent)
|
|
3220
|
+
@projectiles ||= []
|
|
3221
|
+
velocity = Math::Vec3.new(
|
|
3222
|
+
rand * 100.0 - 50.0,
|
|
3223
|
+
rand * 100.0 - 50.0,
|
|
3224
|
+
ent.speed + rand * 200.0
|
|
3225
|
+
)
|
|
3226
|
+
@projectiles << Projectile.new(
|
|
3227
|
+
kind: :fireball,
|
|
3228
|
+
position: ent.position,
|
|
3229
|
+
velocity: velocity,
|
|
3230
|
+
damage: 20,
|
|
3231
|
+
damage_variance: nil,
|
|
3232
|
+
radius_damage: nil,
|
|
3233
|
+
explode_at: @game_time + 5.0,
|
|
3234
|
+
model: "progs/lavaball.mdl",
|
|
3235
|
+
movetype: 6,
|
|
3236
|
+
solid: 1,
|
|
3237
|
+
effects: 0,
|
|
3238
|
+
angles: projectile_angles_from_velocity(velocity),
|
|
3239
|
+
avelocity: Math::Vec3::ORIGIN,
|
|
3240
|
+
voided: false,
|
|
3241
|
+
owner: nil
|
|
3242
|
+
)
|
|
3243
|
+
end
|
|
3244
|
+
|
|
3245
|
+
def fire_bullets(event)
|
|
3246
|
+
return unless @level
|
|
3247
|
+
|
|
3248
|
+
src = @player.position + @player.forward * 10.0
|
|
3249
|
+
# QuakeC FireBullets: src_z = self.absmin_z + self.size_z * 0.7.
|
|
3250
|
+
# Player hull is -24..32, so the shot source is origin z + 15.2.
|
|
3251
|
+
src = Math::Vec3.new(src.x, src.y, @player.position.z + 15.2)
|
|
3252
|
+
right = @player.right
|
|
3253
|
+
up = @player.up
|
|
3254
|
+
solid_brush = @entities.select(&:brush_entity?)
|
|
3255
|
+
multi_ent = nil
|
|
3256
|
+
multi_damage = 0.0
|
|
3257
|
+
blood_position = nil
|
|
3258
|
+
blood_count = 0
|
|
3259
|
+
puff_position = nil
|
|
3260
|
+
puff_count = 0
|
|
3261
|
+
apply_multi_damage = lambda do
|
|
3262
|
+
return unless multi_ent
|
|
3263
|
+
|
|
3264
|
+
damage_entity(multi_ent, multi_damage, attacker: @player_state, inflictor: @player)
|
|
3265
|
+
end
|
|
3266
|
+
|
|
3267
|
+
aim_trace = Physics::HullTrace.trace_world_and_entities(
|
|
3268
|
+
@level, src, src + @player.forward * event.range, solid_brush, hull_num: 0
|
|
3269
|
+
)
|
|
3270
|
+
aim_entity_hit = trace_bullet_touch_entity(src, @player.forward, event.range * aim_trace.fraction)
|
|
3271
|
+
puff_position = ((aim_entity_hit&.last) || aim_trace.end_pos) - @player.forward * 4.0
|
|
3272
|
+
|
|
3273
|
+
event.pellets.times do
|
|
3274
|
+
dir = @player.forward +
|
|
3275
|
+
right * (crandom * event.spread_x) +
|
|
3276
|
+
up * (crandom * event.spread_y)
|
|
3277
|
+
trace = Physics::HullTrace.trace_world_and_entities(
|
|
3278
|
+
@level, src, src + dir * event.range, solid_brush, hull_num: 0
|
|
3279
|
+
)
|
|
3280
|
+
entity_hit = trace_bullet_touch_entity(src, dir, event.range * trace.fraction)
|
|
3281
|
+
if entity_hit
|
|
3282
|
+
ent, hit_pos = entity_hit
|
|
3283
|
+
if ent.damageable?
|
|
3284
|
+
damage = player_damage(event.damage)
|
|
3285
|
+
if ent.equal?(multi_ent)
|
|
3286
|
+
multi_damage += damage
|
|
3287
|
+
else
|
|
3288
|
+
apply_multi_damage.call
|
|
3289
|
+
multi_ent = ent
|
|
3290
|
+
multi_damage = damage
|
|
3291
|
+
end
|
|
3292
|
+
blood_position = hit_pos - dir * 4.0
|
|
3293
|
+
blood_count += 1
|
|
3294
|
+
else
|
|
3295
|
+
puff_count += 1
|
|
3296
|
+
end
|
|
3297
|
+
next
|
|
3298
|
+
end
|
|
3299
|
+
|
|
3300
|
+
next if trace.fraction >= 1.0
|
|
3301
|
+
|
|
3302
|
+
puff_count += 1
|
|
3303
|
+
end
|
|
3304
|
+
|
|
3305
|
+
apply_multi_damage.call
|
|
3306
|
+
@particles&.gunshot(puff_position, count: puff_count) if puff_count.positive?
|
|
3307
|
+
@particles&.blood(blood_position, count: blood_count) if blood_count.positive?
|
|
3308
|
+
end
|
|
3309
|
+
|
|
3310
|
+
def update_projectiles(dt)
|
|
3311
|
+
return unless @projectiles&.any?
|
|
3312
|
+
|
|
3313
|
+
solid_brush = @entities.select(&:brush_entity?)
|
|
3314
|
+
next_projectiles = []
|
|
3315
|
+
@projectiles.each do |projectile|
|
|
3316
|
+
if @game_time >= projectile.explode_at
|
|
3317
|
+
explode_projectile(projectile, projectile.position) if projectile.kind == :grenade
|
|
3318
|
+
next
|
|
3319
|
+
end
|
|
3320
|
+
|
|
3321
|
+
velocity = projectile.velocity
|
|
3322
|
+
avelocity = projectile.avelocity
|
|
3323
|
+
angles = projectile.angles + avelocity * dt
|
|
3324
|
+
# trigger_push_touch also redirects grenades (triggers.qc)
|
|
3325
|
+
if projectile.kind == :grenade
|
|
3326
|
+
push = grenade_push_trigger_velocity(projectile.position)
|
|
3327
|
+
velocity = push if push
|
|
3328
|
+
end
|
|
3329
|
+
velocity += Math::Vec3.new(0.0, 0.0, -current_gravity * dt) if toss_projectile?(projectile)
|
|
3330
|
+
desired = projectile.position + velocity * dt
|
|
3331
|
+
if projectile.voided
|
|
3332
|
+
next_projectiles << projectile.with(position: desired, velocity: velocity, angles: angles)
|
|
3333
|
+
next
|
|
3334
|
+
end
|
|
3335
|
+
|
|
3336
|
+
trace = Physics::HullTrace.trace_world_and_entities(
|
|
3337
|
+
@level, projectile.position, desired, solid_brush, hull_num: 0
|
|
3338
|
+
)
|
|
3339
|
+
segment = desired - projectile.position
|
|
3340
|
+
distance = segment.length
|
|
3341
|
+
dir = distance.zero? ? Math::Vec3::ORIGIN : segment.normalize
|
|
3342
|
+
entity_hit = if spike_projectile?(projectile)
|
|
3343
|
+
trace_spike_touch_entity(projectile.position, dir, distance * trace.fraction, ignore: projectile.owner)
|
|
3344
|
+
elsif laser_projectile?(projectile)
|
|
3345
|
+
trace_laser_touch_entity(projectile.position, dir, distance * trace.fraction, ignore: projectile.owner)
|
|
3346
|
+
elsif projectile.kind == :fireball
|
|
3347
|
+
trace_fireball_touch_entity(projectile.position, dir, distance * trace.fraction, ignore: projectile.owner)
|
|
3348
|
+
elsif projectile.kind == :rocket || projectile.kind == :grenade
|
|
3349
|
+
trace_missile_touch_entity(projectile.position, dir, distance * trace.fraction, ignore: projectile.owner)
|
|
3350
|
+
else
|
|
3351
|
+
trace_damageable_entity(projectile.position, dir, distance * trace.fraction)
|
|
3352
|
+
end
|
|
3353
|
+
if entity_hit
|
|
3354
|
+
ent, hit_pos = entity_hit
|
|
3355
|
+
if projectile.kind == :grenade && !grenade_explodes_on_touch?(ent)
|
|
3356
|
+
@sound_events&.on_grenade_bounce
|
|
3357
|
+
# SV_Physics_Toss reflects off the contact plane (backoff 1.5);
|
|
3358
|
+
# approximate the entity-box face normal from the hit point
|
|
3359
|
+
normal = aabb_face_normal(ent, hit_pos)
|
|
3360
|
+
bounced = normal ? bounce_velocity(velocity, normal) : velocity
|
|
3361
|
+
next_projectiles << projectile.with(
|
|
3362
|
+
position: hit_pos,
|
|
3363
|
+
velocity: bounced,
|
|
3364
|
+
angles: angles,
|
|
3365
|
+
avelocity: grenade_bounce_avelocity(projectile, bounced)
|
|
3366
|
+
)
|
|
3367
|
+
next
|
|
3368
|
+
end
|
|
3369
|
+
|
|
3370
|
+
touch_projectile(projectile, hit_pos, ent)
|
|
3371
|
+
next
|
|
3372
|
+
end
|
|
3373
|
+
|
|
3374
|
+
if trace.fraction < 1.0 && trace.plane_normal
|
|
3375
|
+
impact_pos = trace.end_pos
|
|
3376
|
+
if projectile_removed_by_sky?(projectile, impact_pos)
|
|
3377
|
+
next
|
|
3378
|
+
elsif projectile.kind == :rocket
|
|
3379
|
+
explode_projectile(projectile, impact_pos)
|
|
3380
|
+
next
|
|
3381
|
+
elsif laser_projectile?(projectile)
|
|
3382
|
+
play_laser_impact_sound
|
|
3383
|
+
@particles&.gunshot(laser_impact_position(projectile, impact_pos), count: 5)
|
|
3384
|
+
next
|
|
3385
|
+
elsif spike_projectile?(projectile)
|
|
3386
|
+
play_spike_impact_sound
|
|
3387
|
+
@particles&.gunshot(impact_pos, count: spike_impact_particle_count(projectile))
|
|
3388
|
+
next
|
|
3389
|
+
elsif projectile.kind == :fireball
|
|
3390
|
+
next
|
|
3391
|
+
else
|
|
3392
|
+
velocity = bounce_velocity(velocity, trace.plane_normal)
|
|
3393
|
+
@sound_events&.on_grenade_bounce if projectile.kind == :grenade
|
|
3394
|
+
avelocity = grenade_bounce_avelocity(projectile, velocity)
|
|
3395
|
+
desired = impact_pos
|
|
3396
|
+
end
|
|
3397
|
+
end
|
|
3398
|
+
|
|
3399
|
+
emit_projectile_trail(projectile, desired)
|
|
3400
|
+
next_projectiles << Projectile.new(
|
|
3401
|
+
kind: projectile.kind,
|
|
3402
|
+
position: desired,
|
|
3403
|
+
velocity: velocity,
|
|
3404
|
+
damage: projectile.damage,
|
|
3405
|
+
damage_variance: projectile.damage_variance,
|
|
3406
|
+
radius_damage: projectile.radius_damage,
|
|
3407
|
+
explode_at: projectile.explode_at,
|
|
3408
|
+
model: projectile.model,
|
|
3409
|
+
movetype: projectile.movetype,
|
|
3410
|
+
solid: projectile.solid,
|
|
3411
|
+
effects: projectile.effects,
|
|
3412
|
+
angles: angles,
|
|
3413
|
+
avelocity: avelocity,
|
|
3414
|
+
voided: projectile.voided,
|
|
3415
|
+
owner: projectile.owner
|
|
3416
|
+
)
|
|
3417
|
+
end
|
|
3418
|
+
@projectiles = next_projectiles
|
|
3419
|
+
end
|
|
3420
|
+
|
|
3421
|
+
def emit_projectile_trail(projectile, end_pos)
|
|
3422
|
+
trail_type = projectile_trail_type(projectile)
|
|
3423
|
+
@particles&.rocket_trail(projectile.position, end_pos, type: trail_type) if trail_type
|
|
3424
|
+
end
|
|
3425
|
+
|
|
3426
|
+
def projectile_trail_type(projectile)
|
|
3427
|
+
flags = model_flags(projectile.model)
|
|
3428
|
+
if flags
|
|
3429
|
+
return 2 if (flags & EF_GIB) != 0
|
|
3430
|
+
return 4 if (flags & EF_ZOMGIB) != 0
|
|
3431
|
+
return 3 if (flags & EF_TRACER) != 0
|
|
3432
|
+
return 5 if (flags & EF_TRACER2) != 0
|
|
3433
|
+
return 0 if (flags & EF_ROCKET) != 0
|
|
3434
|
+
return 1 if (flags & EF_GRENADE) != 0
|
|
3435
|
+
return 6 if (flags & EF_TRACER3) != 0
|
|
3436
|
+
end
|
|
3437
|
+
|
|
3438
|
+
case projectile.kind
|
|
3439
|
+
when :rocket then 0
|
|
3440
|
+
when :grenade then 1
|
|
3441
|
+
end
|
|
3442
|
+
end
|
|
3443
|
+
|
|
3444
|
+
def render_projectiles
|
|
3445
|
+
return unless @projectiles&.any?
|
|
3446
|
+
|
|
3447
|
+
@projectiles.each do |projectile|
|
|
3448
|
+
gl_model = @mdl_renderers&.[](projectile.model)
|
|
3449
|
+
next unless gl_model
|
|
3450
|
+
ambient_light, shade_light = alias_entity_lighting(projectile.position)
|
|
3451
|
+
|
|
3452
|
+
gl_model.render(
|
|
3453
|
+
frame_index: 0,
|
|
3454
|
+
lerp: 0.0,
|
|
3455
|
+
position: projectile.position,
|
|
3456
|
+
yaw: projectile.angles.y,
|
|
3457
|
+
pitch: -projectile.angles.x,
|
|
3458
|
+
roll: projectile.angles.z,
|
|
3459
|
+
time: @game_time,
|
|
3460
|
+
ambient_light: ambient_light,
|
|
3461
|
+
shade_light: shade_light
|
|
3462
|
+
)
|
|
3463
|
+
end
|
|
3464
|
+
end
|
|
3465
|
+
|
|
3466
|
+
def render_temp_beams
|
|
3467
|
+
active_temp_beam_segments.each do |segment|
|
|
3468
|
+
gl_model = @mdl_renderers&.[](segment.model)
|
|
3469
|
+
next unless gl_model
|
|
3470
|
+
|
|
3471
|
+
ambient_light, shade_light = alias_entity_lighting(segment.origin)
|
|
3472
|
+
gl_model.render(
|
|
3473
|
+
frame_index: 0,
|
|
3474
|
+
lerp: 0.0,
|
|
3475
|
+
position: segment.origin,
|
|
3476
|
+
yaw: segment.yaw,
|
|
3477
|
+
pitch: -segment.pitch,
|
|
3478
|
+
roll: segment.roll,
|
|
3479
|
+
time: @game_time,
|
|
3480
|
+
ambient_light: ambient_light,
|
|
3481
|
+
shade_light: shade_light
|
|
3482
|
+
)
|
|
3483
|
+
end
|
|
3484
|
+
end
|
|
3485
|
+
|
|
3486
|
+
def active_temp_beam_segments
|
|
3487
|
+
Array(@temporary_beams).flat_map do |beam|
|
|
3488
|
+
next [] if beam.end_time < @game_time.to_f
|
|
3489
|
+
|
|
3490
|
+
beam_segments(beam)
|
|
3491
|
+
end
|
|
3492
|
+
end
|
|
3493
|
+
|
|
3494
|
+
def beam_segments(beam)
|
|
3495
|
+
start = beam_start_position(beam)
|
|
3496
|
+
dist = beam.finish - start
|
|
3497
|
+
length = dist.length
|
|
3498
|
+
return [] if length.zero?
|
|
3499
|
+
|
|
3500
|
+
dir = dist * (1.0 / length)
|
|
3501
|
+
pitch, yaw = quake_vectoangles(dist)
|
|
3502
|
+
origin = start
|
|
3503
|
+
remaining = length
|
|
3504
|
+
segments = []
|
|
3505
|
+
while remaining.positive?
|
|
3506
|
+
segments << TemporaryBeamSegment.new(
|
|
3507
|
+
model: beam.model,
|
|
3508
|
+
origin: origin,
|
|
3509
|
+
pitch: pitch,
|
|
3510
|
+
yaw: yaw,
|
|
3511
|
+
roll: (rand * 360.0).to_i
|
|
3512
|
+
)
|
|
3513
|
+
origin += dir * 30.0
|
|
3514
|
+
remaining -= 30.0
|
|
3515
|
+
end
|
|
3516
|
+
segments
|
|
3517
|
+
end
|
|
3518
|
+
|
|
3519
|
+
def beam_start_position(beam)
|
|
3520
|
+
if beam.entity == VIEW_ENTITY && @player
|
|
3521
|
+
return @player.position + Math::Vec3.new(0.0, 0.0, 16.0)
|
|
3522
|
+
end
|
|
3523
|
+
|
|
3524
|
+
beam.start
|
|
3525
|
+
end
|
|
3526
|
+
|
|
3527
|
+
def alias_entity_lighting(origin, ambient_light: nil, shade_light: nil)
|
|
3528
|
+
base_light = quake_light_point(origin)
|
|
3529
|
+
ambient = (ambient_light || base_light).to_f
|
|
3530
|
+
shade = (shade_light || base_light).to_f
|
|
3531
|
+
@dynamic_lights&.each do |light|
|
|
3532
|
+
next if light.die < @game_time.to_f
|
|
3533
|
+
|
|
3534
|
+
add = light.radius.to_f - (origin - light.origin).length
|
|
3535
|
+
next unless add.positive?
|
|
3536
|
+
|
|
3537
|
+
ambient += add
|
|
3538
|
+
shade += add
|
|
3539
|
+
end
|
|
3540
|
+
[ambient, shade]
|
|
3541
|
+
end
|
|
3542
|
+
|
|
3543
|
+
def quake_light_point(point)
|
|
3544
|
+
return 200 unless @level
|
|
3545
|
+
return 255 if @level.lighting.nil? || @level.lighting.empty?
|
|
3546
|
+
return 0 if @level.nodes.empty? || @level.planes.empty?
|
|
3547
|
+
|
|
3548
|
+
# Static geometry means a static answer per position; most callers
|
|
3549
|
+
# (items, torches, the view entity) barely move, so memoize. Sampled
|
|
3550
|
+
# light also depends on the lightstyle values, so the cache drops
|
|
3551
|
+
# whenever they change (style animation or entity-driven styles).
|
|
3552
|
+
styles = current_lightstyle_values
|
|
3553
|
+
if @light_point_cache_level != @level || @light_point_cache_styles != styles ||
|
|
3554
|
+
@light_point_cache.size > 4096
|
|
3555
|
+
@light_point_cache = {}
|
|
3556
|
+
@light_point_cache_level = @level
|
|
3557
|
+
@light_point_cache_styles = styles.dup
|
|
3558
|
+
end
|
|
3559
|
+
key = [point.x.round, point.y.round, point.z.round]
|
|
3560
|
+
@light_point_cache.fetch(key) do
|
|
3561
|
+
finish = point - Math::Vec3.new(0.0, 0.0, 8192.0 + 2.0)
|
|
3562
|
+
light = recursive_light_point(0, point, finish)
|
|
3563
|
+
@light_point_cache[key] = (light == -1 ? 0 : light)
|
|
3564
|
+
end
|
|
3565
|
+
end
|
|
3566
|
+
|
|
3567
|
+
def recursive_light_point(node_index, start, finish)
|
|
3568
|
+
return -1 if node_index.negative?
|
|
3569
|
+
|
|
3570
|
+
node = @level.nodes[node_index]
|
|
3571
|
+
return -1 unless node
|
|
3572
|
+
|
|
3573
|
+
plane = @level.planes[node.plane_index]
|
|
3574
|
+
return -1 unless plane
|
|
3575
|
+
|
|
3576
|
+
front = start.dot(plane.normal) - plane.dist
|
|
3577
|
+
back = finish.dot(plane.normal) - plane.dist
|
|
3578
|
+
side = front.negative? ? 1 : 0
|
|
3579
|
+
|
|
3580
|
+
return recursive_light_point(node.children[side], start, finish) if back.negative? == (side == 1)
|
|
3581
|
+
|
|
3582
|
+
frac = front / (front - back)
|
|
3583
|
+
mid = start + (finish - start) * frac
|
|
3584
|
+
|
|
3585
|
+
light = recursive_light_point(node.children[side], start, mid)
|
|
3586
|
+
return light if light >= 0
|
|
3587
|
+
return -1 if back.negative? == (side == 1)
|
|
3588
|
+
|
|
3589
|
+
sampled = light_point_from_node_surfaces(node, mid)
|
|
3590
|
+
return sampled unless sampled.nil?
|
|
3591
|
+
|
|
3592
|
+
recursive_light_point(node.children[1 - side], mid, finish)
|
|
3593
|
+
end
|
|
3594
|
+
|
|
3595
|
+
def light_point_from_node_surfaces(node, point)
|
|
3596
|
+
first_face = node.first_face
|
|
3597
|
+
last_face = first_face + node.num_faces
|
|
3598
|
+
(first_face...last_face).each do |face_index|
|
|
3599
|
+
face = @level.faces[face_index]
|
|
3600
|
+
next unless face
|
|
3601
|
+
next if (face.flags & Bsp::Face::SURF_DRAWTILED) != 0
|
|
3602
|
+
|
|
3603
|
+
texinfo = @level.texinfo[face.texinfo_index]
|
|
3604
|
+
next unless texinfo
|
|
3605
|
+
|
|
3606
|
+
s = point.dot(texinfo.s_vec) + texinfo.s_offset
|
|
3607
|
+
t = point.dot(texinfo.t_vec) + texinfo.t_offset
|
|
3608
|
+
mins = face.texture_mins || [0, 0]
|
|
3609
|
+
extents = face.extents || [0, 0]
|
|
3610
|
+
next if s < mins[0] || t < mins[1]
|
|
3611
|
+
|
|
3612
|
+
ds = s - mins[0]
|
|
3613
|
+
dt = t - mins[1]
|
|
3614
|
+
next if ds > extents[0] || dt > extents[1]
|
|
3615
|
+
|
|
3616
|
+
return 0 if face.light_offset.negative?
|
|
3617
|
+
|
|
3618
|
+
return sample_surface_light(face, ds.to_i >> 4, dt.to_i >> 4)
|
|
3619
|
+
end
|
|
3620
|
+
nil
|
|
3621
|
+
end
|
|
3622
|
+
|
|
3623
|
+
def sample_surface_light(face, ds, dt)
|
|
3624
|
+
width = (face.extents[0] >> 4) + 1
|
|
3625
|
+
height = (face.extents[1] >> 4) + 1
|
|
3626
|
+
lightmap_size = width * height
|
|
3627
|
+
offset = face.light_offset + dt * width + ds
|
|
3628
|
+
light = 0
|
|
3629
|
+
|
|
3630
|
+
face.styles.each do |style|
|
|
3631
|
+
break if style == 255
|
|
3632
|
+
|
|
3633
|
+
sample = @level.lighting.getbyte(offset)
|
|
3634
|
+
light += sample * current_lightstyle_values.fetch(style, 256) if sample
|
|
3635
|
+
offset += lightmap_size
|
|
3636
|
+
end
|
|
3637
|
+
|
|
3638
|
+
light >> 8
|
|
3639
|
+
end
|
|
3640
|
+
|
|
3641
|
+
def current_lightstyle_values
|
|
3642
|
+
@lightmap&.lightstyle_values ||
|
|
3643
|
+
Renderer::GLLightmap.lightstyle_values_for((@game_time || 0.0).to_f, current_lightstyles)
|
|
3644
|
+
end
|
|
3645
|
+
|
|
3646
|
+
def emit_entity_effect_particles(ent)
|
|
3647
|
+
# NOTE: effects may be nil, and nil & 1 == false in Ruby, which != 0
|
|
3648
|
+
return unless (ent.effects.to_i & ENTITY_EFFECT_BRIGHTFIELD) != 0
|
|
3649
|
+
|
|
3650
|
+
@particles&.entity_particles(ent.position, time: @game_time.to_f)
|
|
3651
|
+
end
|
|
3652
|
+
|
|
3653
|
+
def current_view_blend
|
|
3654
|
+
contents = point_contents(@camera&.position || player_eye_position)
|
|
3655
|
+
Renderer::GLViewBlend.calc_blend(
|
|
3656
|
+
[
|
|
3657
|
+
Renderer::GLViewBlend.contents_cshift(contents),
|
|
3658
|
+
@damage_cshift || Renderer::GLViewBlend::CShift.new(color: [0, 0, 0], percent: 0),
|
|
3659
|
+
Renderer::GLViewBlend.bonus_cshift(@bonus_cshift_percent.to_f),
|
|
3660
|
+
Renderer::GLViewBlend.powerup_cshift(@player_state, game_time: @game_time.to_f)
|
|
3661
|
+
]
|
|
3662
|
+
)
|
|
3663
|
+
end
|
|
3664
|
+
|
|
3665
|
+
def trigger_bonus_flash
|
|
3666
|
+
@sound_events&.on_player(:bonus_flash)
|
|
3667
|
+
@bonus_cshift_percent = 50.0
|
|
3668
|
+
end
|
|
3669
|
+
|
|
3670
|
+
def update_view_cshifts(dt)
|
|
3671
|
+
@bonus_cshift_percent = [@bonus_cshift_percent.to_f - dt.to_f * 100.0, 0.0].max
|
|
3672
|
+
damage = @damage_cshift || Renderer::GLViewBlend::CShift.new(color: [0, 0, 0], percent: 0.0)
|
|
3673
|
+
@damage_cshift = Renderer::GLViewBlend::CShift.new(
|
|
3674
|
+
color: damage.color,
|
|
3675
|
+
percent: [damage.percent.to_f - dt.to_f * 150.0, 0.0].max
|
|
3676
|
+
)
|
|
3677
|
+
@damage_kick_time = [@damage_kick_time.to_f - dt.to_f, 0.0].max
|
|
3678
|
+
update_weapon_punch_angle(dt)
|
|
3679
|
+
end
|
|
3680
|
+
|
|
3681
|
+
def trigger_weapon_punch(event)
|
|
3682
|
+
@weapon_punch_angle = WEAPON_PUNCH_ANGLES.fetch(event.kick, Math::Vec3::ORIGIN)
|
|
3683
|
+
end
|
|
3684
|
+
|
|
3685
|
+
def update_weapon_punch_angle(dt)
|
|
3686
|
+
punch = current_weapon_punch_angle
|
|
3687
|
+
len = punch.length
|
|
3688
|
+
return if len.zero?
|
|
3689
|
+
|
|
3690
|
+
len = [len - WEAPON_PUNCH_DECAY * dt.to_f, 0.0].max
|
|
3691
|
+
@weapon_punch_angle = len.zero? ? Math::Vec3::ORIGIN : punch.normalize * len
|
|
3692
|
+
end
|
|
3693
|
+
|
|
3694
|
+
def current_weapon_punch_angle
|
|
3695
|
+
@weapon_punch_angle || Math::Vec3::ORIGIN
|
|
3696
|
+
end
|
|
3697
|
+
|
|
3698
|
+
def trigger_damage_flash(result)
|
|
3699
|
+
armor = result[:save].to_f
|
|
3700
|
+
blood = result.fetch(:display_take, result[:take]).to_f
|
|
3701
|
+
return if armor <= 0.0 && blood <= 0.0
|
|
3702
|
+
|
|
3703
|
+
@player_state.face_anim_time = @game_time.to_f + 0.2 if @player_state
|
|
3704
|
+
@damage_cshift = Renderer::GLViewBlend.damage_cshift(
|
|
3705
|
+
armor: armor,
|
|
3706
|
+
blood: blood,
|
|
3707
|
+
current_percent: @damage_cshift&.percent.to_f
|
|
3708
|
+
)
|
|
3709
|
+
end
|
|
3710
|
+
|
|
3711
|
+
def trigger_damage_kick(result, inflictor)
|
|
3712
|
+
source = damage_kick_source(inflictor)
|
|
3713
|
+
return unless @player && source
|
|
3714
|
+
|
|
3715
|
+
from = source - @player.position
|
|
3716
|
+
return if from.length.zero?
|
|
3717
|
+
|
|
3718
|
+
from = from.normalize
|
|
3719
|
+
blood = result.fetch(:display_take, result[:take]).to_f
|
|
3720
|
+
count = blood * 0.5 + result[:save].to_f * 0.5
|
|
3721
|
+
count = 10.0 if count < 10.0
|
|
3722
|
+
@damage_kick_roll = count * from.dot(@player.right) * 0.6
|
|
3723
|
+
@damage_kick_pitch = count * from.dot(@player.forward) * 0.6
|
|
3724
|
+
@damage_kick_time = 0.5
|
|
3725
|
+
end
|
|
3726
|
+
|
|
3727
|
+
def damage_kick_source(inflictor)
|
|
3728
|
+
return inflictor if inflictor.is_a?(Math::Vec3)
|
|
3729
|
+
return inflictor.position if inflictor.respond_to?(:position)
|
|
3730
|
+
|
|
3731
|
+
nil
|
|
3732
|
+
end
|
|
3733
|
+
|
|
3734
|
+
def current_damage_kick_angles
|
|
3735
|
+
return [0.0, 0.0] unless @damage_kick_time.to_f.positive?
|
|
3736
|
+
|
|
3737
|
+
scale = @damage_kick_time.to_f / 0.5
|
|
3738
|
+
[@damage_kick_roll.to_f * scale, @damage_kick_pitch.to_f * scale]
|
|
3739
|
+
end
|
|
3740
|
+
|
|
3741
|
+
def build_dynamic_lights
|
|
3742
|
+
lights = []
|
|
3743
|
+
@entities&.each_with_index do |ent, index|
|
|
3744
|
+
next if ent.removed?
|
|
3745
|
+
|
|
3746
|
+
lights.concat(quake_effect_dynamic_lights(
|
|
3747
|
+
ent.effects,
|
|
3748
|
+
origin: ent.position,
|
|
3749
|
+
angles: ent.angles,
|
|
3750
|
+
key: index + 1
|
|
3751
|
+
))
|
|
3752
|
+
end
|
|
3753
|
+
@projectiles&.each_with_index do |projectile, index|
|
|
3754
|
+
lights.concat(quake_effect_dynamic_lights(
|
|
3755
|
+
projectile.effects,
|
|
3756
|
+
origin: projectile.position,
|
|
3757
|
+
angles: projectile.angles,
|
|
3758
|
+
key: 10_000 + index
|
|
3759
|
+
))
|
|
3760
|
+
end
|
|
3761
|
+
@temporary_dynamic_lights = Array(@temporary_dynamic_lights).filter_map do |light|
|
|
3762
|
+
next if light.die < @game_time.to_f
|
|
3763
|
+
|
|
3764
|
+
radius = light.radius.to_f - (@game_time.to_f - light.start_time.to_f) * light.decay.to_f
|
|
3765
|
+
next if radius <= 0.0
|
|
3766
|
+
|
|
3767
|
+
lights << DynamicLight.new(
|
|
3768
|
+
key: 0,
|
|
3769
|
+
origin: light.origin,
|
|
3770
|
+
radius: radius,
|
|
3771
|
+
minlight: light.minlight,
|
|
3772
|
+
die: light.die
|
|
3773
|
+
)
|
|
3774
|
+
light
|
|
3775
|
+
end
|
|
3776
|
+
@dynamic_lights = lights
|
|
3777
|
+
end
|
|
3778
|
+
|
|
3779
|
+
# CL_MuzzleFlash: dlight 18 units ahead of the eyes, radius
|
|
3780
|
+
# 200 + rand&31, minlight 32, dies after 0.1s
|
|
3781
|
+
def add_muzzle_flash_dynamic_light
|
|
3782
|
+
return unless @player
|
|
3783
|
+
|
|
3784
|
+
origin = player_eye_position + @player.forward * 18.0
|
|
3785
|
+
@temporary_dynamic_lights ||= []
|
|
3786
|
+
@temporary_dynamic_lights << TemporaryDynamicLight.new(
|
|
3787
|
+
origin: origin,
|
|
3788
|
+
radius: 200.0 + rand * 32.0,
|
|
3789
|
+
minlight: 32.0,
|
|
3790
|
+
start_time: @game_time.to_f,
|
|
3791
|
+
die: @game_time.to_f + 0.1,
|
|
3792
|
+
decay: 0.0
|
|
3793
|
+
)
|
|
3794
|
+
end
|
|
3795
|
+
|
|
3796
|
+
def add_temp_explosion_dynamic_light(origin)
|
|
3797
|
+
@temporary_dynamic_lights ||= []
|
|
3798
|
+
@temporary_dynamic_lights << TemporaryDynamicLight.new(
|
|
3799
|
+
origin: origin,
|
|
3800
|
+
radius: 350.0,
|
|
3801
|
+
minlight: 0.0,
|
|
3802
|
+
start_time: @game_time.to_f,
|
|
3803
|
+
die: @game_time.to_f + 0.5,
|
|
3804
|
+
decay: 300.0
|
|
3805
|
+
)
|
|
3806
|
+
end
|
|
3807
|
+
|
|
3808
|
+
def spawn_temp_color_mapped_explosion(origin, color_start:, color_length:)
|
|
3809
|
+
@particles&.color_mapped_explosion(origin, color_start: color_start, color_length: color_length)
|
|
3810
|
+
add_temp_explosion_dynamic_light(origin)
|
|
3811
|
+
play_temp_explosion_sound
|
|
3812
|
+
end
|
|
3813
|
+
|
|
3814
|
+
def spawn_temp_tar_explosion(origin)
|
|
3815
|
+
@particles&.blob_explosion(origin)
|
|
3816
|
+
play_temp_explosion_sound
|
|
3817
|
+
end
|
|
3818
|
+
|
|
3819
|
+
def spawn_temp_spike_impact(origin)
|
|
3820
|
+
@particles&.gunshot(origin, count: 10)
|
|
3821
|
+
play_spike_impact_sound
|
|
3822
|
+
end
|
|
3823
|
+
|
|
3824
|
+
def spawn_temp_super_spike_impact(origin)
|
|
3825
|
+
@particles&.gunshot(origin, count: 20)
|
|
3826
|
+
play_spike_impact_sound
|
|
3827
|
+
end
|
|
3828
|
+
|
|
3829
|
+
def spawn_temp_wizard_spike_impact(origin)
|
|
3830
|
+
@particles&.run_particle_effect(origin, color: 20, count: 30)
|
|
3831
|
+
@sound_events&.on_weapon_fire(WeaponSound.new(sound: "wizard/hit.wav"))
|
|
3832
|
+
end
|
|
3833
|
+
|
|
3834
|
+
def spawn_temp_knight_spike_impact(origin)
|
|
3835
|
+
@particles&.run_particle_effect(origin, color: 226, count: 20)
|
|
3836
|
+
@sound_events&.on_weapon_fire(WeaponSound.new(sound: "hknight/hit.wav"))
|
|
3837
|
+
end
|
|
3838
|
+
|
|
3839
|
+
def spawn_temp_gunshot_impact(origin)
|
|
3840
|
+
@particles&.gunshot(origin, count: 20)
|
|
3841
|
+
end
|
|
3842
|
+
|
|
3843
|
+
def quake_effect_dynamic_lights(effects, origin:, angles:, key:)
|
|
3844
|
+
light = nil
|
|
3845
|
+
effects = effects.to_i
|
|
3846
|
+
|
|
3847
|
+
if (effects & ENTITY_EFFECT_MUZZLEFLASH) != 0
|
|
3848
|
+
light = DynamicLight.new(
|
|
3849
|
+
key: key,
|
|
3850
|
+
origin: origin + Math::Vec3.new(0.0, 0.0, 16.0) + angle_forward(angles) * 18.0,
|
|
3851
|
+
radius: 200.0 + quake_effect_light_jitter,
|
|
3852
|
+
minlight: 32.0,
|
|
3853
|
+
die: @game_time.to_f + 0.1
|
|
3854
|
+
)
|
|
3855
|
+
end
|
|
3856
|
+
|
|
3857
|
+
if (effects & ENTITY_EFFECT_BRIGHTLIGHT) != 0
|
|
3858
|
+
light = DynamicLight.new(
|
|
3859
|
+
key: key,
|
|
3860
|
+
origin: origin + Math::Vec3.new(0.0, 0.0, 16.0),
|
|
3861
|
+
radius: 400.0 + quake_effect_light_jitter,
|
|
3862
|
+
minlight: 0.0,
|
|
3863
|
+
die: @game_time.to_f + 0.001
|
|
3864
|
+
)
|
|
3865
|
+
end
|
|
3866
|
+
|
|
3867
|
+
if (effects & ENTITY_EFFECT_DIMLIGHT) != 0
|
|
3868
|
+
light = DynamicLight.new(
|
|
3869
|
+
key: key,
|
|
3870
|
+
origin: origin,
|
|
3871
|
+
radius: 200.0 + quake_effect_light_jitter,
|
|
3872
|
+
minlight: 0.0,
|
|
3873
|
+
die: @game_time.to_f + 0.001
|
|
3874
|
+
)
|
|
3875
|
+
end
|
|
3876
|
+
|
|
3877
|
+
light ? [light] : []
|
|
3878
|
+
end
|
|
3879
|
+
|
|
3880
|
+
def quake_effect_light_jitter
|
|
3881
|
+
(rand * 32.0).to_i
|
|
3882
|
+
end
|
|
3883
|
+
|
|
3884
|
+
def projectile_angles_from_velocity(velocity)
|
|
3885
|
+
pitch, yaw = quake_vectoangles(velocity)
|
|
3886
|
+
Math::Vec3.new(pitch, yaw, 0.0)
|
|
3887
|
+
end
|
|
3888
|
+
|
|
3889
|
+
def alias_entity_yaw(ent, model_path)
|
|
3890
|
+
flags = model_flags(model_path)
|
|
3891
|
+
return quake_anglemod(100.0 * @game_time.to_f) if flags && (flags & EF_ROTATE) != 0
|
|
3892
|
+
|
|
3893
|
+
ent.angles == Math::Vec3::ORIGIN ? ent.angle : ent.angles.y
|
|
3894
|
+
end
|
|
3895
|
+
|
|
3896
|
+
# Per-entity random animation phase (entity_t.syncbase) so identical
|
|
3897
|
+
# ST_RAND models (torches, flames) don't animate in lockstep.
|
|
3898
|
+
def entity_sync_base(ent, gl_model)
|
|
3899
|
+
return 0.0 unless gl_model.random_sync?
|
|
3900
|
+
|
|
3901
|
+
@entity_sync_bases ||= {}
|
|
3902
|
+
@entity_sync_bases[ent.object_id] ||= rand
|
|
3903
|
+
end
|
|
3904
|
+
|
|
3905
|
+
def model_flags(model_path)
|
|
3906
|
+
@mdl_cache&.[](model_path)&.flags
|
|
3907
|
+
end
|
|
3908
|
+
|
|
3909
|
+
def quake_anglemod(angle)
|
|
3910
|
+
(360.0 / 65_536.0) * ((angle * (65_536.0 / 360.0)).to_i & 65_535)
|
|
3911
|
+
end
|
|
3912
|
+
|
|
3913
|
+
def quake_vectoangles(vec)
|
|
3914
|
+
if vec.y.zero? && vec.x.zero?
|
|
3915
|
+
pitch = vec.z.positive? ? 90 : 270
|
|
3916
|
+
return [pitch, 0]
|
|
3917
|
+
end
|
|
3918
|
+
|
|
3919
|
+
yaw = (::Math.atan2(vec.y, vec.x) * 180.0 / ::Math::PI).to_i
|
|
3920
|
+
yaw += 360 if yaw.negative?
|
|
3921
|
+
forward = ::Math.sqrt(vec.x * vec.x + vec.y * vec.y)
|
|
3922
|
+
pitch = (::Math.atan2(vec.z, forward) * 180.0 / ::Math::PI).to_i
|
|
3923
|
+
pitch += 360 if pitch.negative?
|
|
3924
|
+
[pitch, yaw]
|
|
3925
|
+
end
|
|
3926
|
+
|
|
3927
|
+
def touch_projectile(projectile, position, ent)
|
|
3928
|
+
return if projectile_removed_by_sky?(projectile, position)
|
|
3929
|
+
|
|
3930
|
+
touch_inflictor = projectile.with(position: position)
|
|
3931
|
+
if projectile.kind == :rocket
|
|
3932
|
+
explode_projectile(projectile, position, direct_hit: ent)
|
|
3933
|
+
elsif spike_projectile?(projectile)
|
|
3934
|
+
if ent.damageable?
|
|
3935
|
+
ent.instance_variable_set(:@deathtype, projectile_deathtype(projectile))
|
|
3936
|
+
damage_entity(
|
|
3937
|
+
ent,
|
|
3938
|
+
projectile_owner_damage(projectile, projectile.damage),
|
|
3939
|
+
attacker: projectile.owner,
|
|
3940
|
+
inflictor: touch_inflictor
|
|
3941
|
+
)
|
|
3942
|
+
@particles&.blood(position, count: projectile_touch_blood_count(projectile))
|
|
3943
|
+
else
|
|
3944
|
+
play_spike_impact_sound
|
|
3945
|
+
@particles&.gunshot(position, count: spike_impact_particle_count(projectile))
|
|
3946
|
+
end
|
|
3947
|
+
elsif laser_projectile?(projectile)
|
|
3948
|
+
play_laser_impact_sound
|
|
3949
|
+
impact = laser_impact_position(projectile, position)
|
|
3950
|
+
if ent.health != 0.0
|
|
3951
|
+
ent.instance_variable_set(:@deathtype, "laser")
|
|
3952
|
+
damage_entity(
|
|
3953
|
+
ent,
|
|
3954
|
+
projectile_owner_damage(projectile, projectile.damage),
|
|
3955
|
+
attacker: projectile.owner,
|
|
3956
|
+
inflictor: touch_inflictor
|
|
3957
|
+
)
|
|
3958
|
+
@particles&.blood(impact, count: projectile_touch_blood_count(projectile))
|
|
3959
|
+
else
|
|
3960
|
+
@particles&.gunshot(impact, count: 5)
|
|
3961
|
+
end
|
|
3962
|
+
elsif projectile.kind == :fireball
|
|
3963
|
+
damage_entity(
|
|
3964
|
+
ent,
|
|
3965
|
+
projectile.damage,
|
|
3966
|
+
attacker: touch_inflictor,
|
|
3967
|
+
inflictor: touch_inflictor
|
|
3968
|
+
)
|
|
3969
|
+
else
|
|
3970
|
+
explode_projectile(projectile, position)
|
|
3971
|
+
end
|
|
3972
|
+
end
|
|
3973
|
+
|
|
3974
|
+
def grenade_explodes_on_touch?(ent)
|
|
3975
|
+
takedamage = ent.instance_variable_get(:@takedamage)
|
|
3976
|
+
takedamage == 2
|
|
3977
|
+
end
|
|
3978
|
+
|
|
3979
|
+
def grenade_bounce_avelocity(projectile, velocity)
|
|
3980
|
+
return projectile.avelocity unless projectile.kind == :grenade
|
|
3981
|
+
|
|
3982
|
+
velocity == Math::Vec3::ORIGIN ? Math::Vec3::ORIGIN : projectile.avelocity
|
|
3983
|
+
end
|
|
3984
|
+
|
|
3985
|
+
def spike_projectile?(projectile)
|
|
3986
|
+
projectile.kind == :spike || projectile.kind == :super_spike
|
|
3987
|
+
end
|
|
3988
|
+
|
|
3989
|
+
def laser_projectile?(projectile)
|
|
3990
|
+
projectile.kind == :laser
|
|
3991
|
+
end
|
|
3992
|
+
|
|
3993
|
+
def projectile_touched_sky?(position)
|
|
3994
|
+
point_contents(position) == Physics::CONTENTS_SKY
|
|
3995
|
+
end
|
|
3996
|
+
|
|
3997
|
+
def projectile_removed_by_sky?(projectile, position)
|
|
3998
|
+
(projectile.kind == :rocket || spike_projectile?(projectile) || laser_projectile?(projectile)) &&
|
|
3999
|
+
projectile_touched_sky?(position)
|
|
4000
|
+
end
|
|
4001
|
+
|
|
4002
|
+
def point_contents(position)
|
|
4003
|
+
return Physics::CONTENTS_EMPTY unless @level
|
|
4004
|
+
return Physics::CONTENTS_EMPTY if @level.nodes.empty? || @level.planes.empty? || @level.leafs.empty?
|
|
4005
|
+
|
|
4006
|
+
leaf = @level.leafs[Bsp::Vis.point_in_leaf(@level, position)]
|
|
4007
|
+
leaf ? leaf.contents : Physics::CONTENTS_EMPTY
|
|
4008
|
+
end
|
|
4009
|
+
|
|
4010
|
+
def play_laser_impact_sound
|
|
4011
|
+
@sound_events&.on_weapon_fire(WeaponSound.new(sound: "enforcer/enfstop.wav"))
|
|
4012
|
+
end
|
|
4013
|
+
|
|
4014
|
+
def play_spike_impact_sound
|
|
4015
|
+
sound = if (rand * 5).to_i != 0
|
|
4016
|
+
"weapons/tink1.wav"
|
|
4017
|
+
else
|
|
4018
|
+
case (rand * 4).to_i
|
|
4019
|
+
when 1 then "weapons/ric1.wav"
|
|
4020
|
+
when 2 then "weapons/ric2.wav"
|
|
4021
|
+
else "weapons/ric3.wav"
|
|
4022
|
+
end
|
|
4023
|
+
end
|
|
4024
|
+
@sound_events&.on_weapon_fire(WeaponSound.new(sound: sound))
|
|
4025
|
+
end
|
|
4026
|
+
|
|
4027
|
+
def spike_impact_particle_count(projectile)
|
|
4028
|
+
projectile.kind == :super_spike ? 20 : 10
|
|
4029
|
+
end
|
|
4030
|
+
|
|
4031
|
+
def projectile_touch_blood_count(projectile)
|
|
4032
|
+
case projectile.kind
|
|
4033
|
+
when :spike then 9
|
|
4034
|
+
when :super_spike then 18
|
|
4035
|
+
when :laser then 15
|
|
4036
|
+
else 1
|
|
4037
|
+
end
|
|
4038
|
+
end
|
|
4039
|
+
|
|
4040
|
+
def laser_impact_position(projectile, position)
|
|
4041
|
+
position - projectile.velocity.normalize * 8.0
|
|
4042
|
+
end
|
|
4043
|
+
|
|
4044
|
+
def toss_projectile?(projectile)
|
|
4045
|
+
projectile.kind == :grenade || projectile.kind == :fireball
|
|
4046
|
+
end
|
|
4047
|
+
|
|
4048
|
+
def explode_projectile(projectile, position, direct_hit: nil)
|
|
4049
|
+
explosion_inflictor = projectile.with(position: position)
|
|
4050
|
+
if direct_hit && projectile.kind == :rocket && direct_hit.health.to_f.nonzero?
|
|
4051
|
+
direct_damage = projectile.damage + rand * projectile.damage_variance.to_f
|
|
4052
|
+
direct_hit.instance_variable_set(:@deathtype, "rocket")
|
|
4053
|
+
damage_entity(
|
|
4054
|
+
direct_hit,
|
|
4055
|
+
projectile_owner_damage(projectile, direct_damage),
|
|
4056
|
+
attacker: projectile.owner,
|
|
4057
|
+
inflictor: explosion_inflictor
|
|
4058
|
+
)
|
|
4059
|
+
end
|
|
4060
|
+
|
|
4061
|
+
radius_ignore = projectile.kind == :rocket ? direct_hit : nil
|
|
4062
|
+
apply_radius_damage(
|
|
4063
|
+
position,
|
|
4064
|
+
projectile.radius_damage,
|
|
4065
|
+
ignore: radius_ignore,
|
|
4066
|
+
attacker: projectile.owner,
|
|
4067
|
+
attacker_player: player_attacker?(projectile.owner),
|
|
4068
|
+
inflictor: explosion_inflictor,
|
|
4069
|
+
deathtype: projectile_radius_deathtype(projectile)
|
|
4070
|
+
)
|
|
4071
|
+
play_temp_explosion_sound
|
|
4072
|
+
effect_position = explosion_effect_position(projectile, position)
|
|
4073
|
+
add_temp_explosion_dynamic_light(effect_position)
|
|
4074
|
+
@particles&.explosion(effect_position)
|
|
4075
|
+
end
|
|
4076
|
+
|
|
4077
|
+
def explosion_effect_position(projectile, position)
|
|
4078
|
+
return position unless projectile.kind == :rocket
|
|
4079
|
+
|
|
4080
|
+
position - projectile.velocity.normalize * 8.0
|
|
4081
|
+
end
|
|
4082
|
+
|
|
4083
|
+
def bounce_velocity(velocity, normal)
|
|
4084
|
+
backoff = velocity.dot(normal) * 1.5
|
|
4085
|
+
Math::Vec3.new(
|
|
4086
|
+
stop_epsilon_component(velocity.x - normal.x * backoff),
|
|
4087
|
+
stop_epsilon_component(velocity.y - normal.y * backoff),
|
|
4088
|
+
stop_epsilon_component(velocity.z - normal.z * backoff)
|
|
4089
|
+
)
|
|
4090
|
+
end
|
|
4091
|
+
|
|
4092
|
+
# Nearest box face to the hit point approximates the contact normal
|
|
4093
|
+
# for projectiles bouncing off bbox entities.
|
|
4094
|
+
def aabb_face_normal(ent, hit_pos)
|
|
4095
|
+
mins, maxs = damageable_bounds(ent)
|
|
4096
|
+
return nil unless mins && maxs
|
|
4097
|
+
|
|
4098
|
+
[
|
|
4099
|
+
[(hit_pos.x - mins.x).abs, Math::Vec3.new(-1.0, 0.0, 0.0)],
|
|
4100
|
+
[(maxs.x - hit_pos.x).abs, Math::Vec3.new(1.0, 0.0, 0.0)],
|
|
4101
|
+
[(hit_pos.y - mins.y).abs, Math::Vec3.new(0.0, -1.0, 0.0)],
|
|
4102
|
+
[(maxs.y - hit_pos.y).abs, Math::Vec3.new(0.0, 1.0, 0.0)],
|
|
4103
|
+
[(hit_pos.z - mins.z).abs, Math::Vec3.new(0.0, 0.0, -1.0)],
|
|
4104
|
+
[(maxs.z - hit_pos.z).abs, Math::Vec3.new(0.0, 0.0, 1.0)]
|
|
4105
|
+
].min_by(&:first).last
|
|
4106
|
+
end
|
|
4107
|
+
|
|
4108
|
+
def stop_epsilon_component(value)
|
|
4109
|
+
value > -STOP_EPSILON && value < STOP_EPSILON ? 0.0 : value
|
|
4110
|
+
end
|
|
4111
|
+
|
|
4112
|
+
def apply_radius_damage(origin, damage, ignore: nil, attacker_player: true, attacker: nil, inflictor: nil,
|
|
4113
|
+
deathtype: nil)
|
|
4114
|
+
radius_attacker = attacker || (attacker_player ? @player_state : nil)
|
|
4115
|
+
radius = damage + 40.0
|
|
4116
|
+
if @player && @player_state&.alive?
|
|
4117
|
+
player_center = @player.position + (ENTITY_HULL_MINS + ENTITY_HULL_MAXS) * 0.5
|
|
4118
|
+
points = radius_damage_points(origin, player_center, damage, radius)
|
|
4119
|
+
if points.positive? && can_radius_damage_position?(@player.position, origin)
|
|
4120
|
+
points *= 0.5 if attacker_player
|
|
4121
|
+
player_points = player_damage(points)
|
|
4122
|
+
apply_player_damage_momentum(player_points, inflictor || origin)
|
|
4123
|
+
@player_state.deathtype = deathtype if deathtype
|
|
4124
|
+
damage_player(
|
|
4125
|
+
player_points,
|
|
4126
|
+
pain_sound: true,
|
|
4127
|
+
inflictor: inflictor || origin,
|
|
4128
|
+
attacker: radius_attacker,
|
|
4129
|
+
apply_momentum: false
|
|
4130
|
+
)
|
|
4131
|
+
end
|
|
4132
|
+
end
|
|
4133
|
+
|
|
4134
|
+
@entities.each do |ent|
|
|
4135
|
+
next unless ent.damageable?
|
|
4136
|
+
next if ent.instance_variable_defined?(:@solid) && ent.instance_variable_get(:@solid) == SOLID_NOT
|
|
4137
|
+
next if ent.brush_entity? && !damageable_brush_entity?(ent)
|
|
4138
|
+
next if ent.equal?(ignore)
|
|
4139
|
+
|
|
4140
|
+
damage_pos = damageable_position(ent)
|
|
4141
|
+
points = radius_damage_points(origin, radius_damage_center(ent), damage, radius)
|
|
4142
|
+
if points.positive? && can_radius_damage_entity?(ent, damage_pos, origin)
|
|
4143
|
+
points *= 0.5 if ent.equal?(radius_attacker)
|
|
4144
|
+
ent.instance_variable_set(:@deathtype, deathtype) if deathtype
|
|
4145
|
+
damage_entity(
|
|
4146
|
+
ent,
|
|
4147
|
+
radius_attacker_damage(points, attacker_player),
|
|
4148
|
+
attacker: radius_attacker,
|
|
4149
|
+
inflictor: inflictor || origin
|
|
4150
|
+
)
|
|
4151
|
+
end
|
|
4152
|
+
end
|
|
4153
|
+
end
|
|
4154
|
+
|
|
4155
|
+
def radius_damage_points(origin, center, damage, radius)
|
|
4156
|
+
distance = (origin - center).length
|
|
4157
|
+
return 0.0 if distance > radius
|
|
4158
|
+
|
|
4159
|
+
damage - 0.5 * distance
|
|
4160
|
+
end
|
|
4161
|
+
|
|
4162
|
+
def can_radius_damage_position?(position, origin)
|
|
4163
|
+
return true unless @level
|
|
4164
|
+
|
|
4165
|
+
offsets = [
|
|
4166
|
+
Math::Vec3::ORIGIN,
|
|
4167
|
+
Math::Vec3.new(15.0, 15.0, 0.0),
|
|
4168
|
+
Math::Vec3.new(-15.0, -15.0, 0.0),
|
|
4169
|
+
Math::Vec3.new(-15.0, 15.0, 0.0),
|
|
4170
|
+
Math::Vec3.new(15.0, -15.0, 0.0)
|
|
4171
|
+
]
|
|
4172
|
+
solid_brush = @entities.select(&:brush_entity?)
|
|
4173
|
+
offsets.any? do |offset|
|
|
4174
|
+
trace = Physics::HullTrace.trace_world_and_entities(
|
|
4175
|
+
@level, origin, position + offset, solid_brush, hull_num: 0
|
|
4176
|
+
)
|
|
4177
|
+
trace.fraction >= 1.0
|
|
4178
|
+
end
|
|
4179
|
+
end
|
|
4180
|
+
|
|
4181
|
+
def can_radius_damage_entity?(ent, position, origin)
|
|
4182
|
+
return true unless @level
|
|
4183
|
+
return can_radius_damage_position?(position, origin) unless ent.brush_entity?
|
|
4184
|
+
|
|
4185
|
+
solid_brush = @entities.select(&:brush_entity?)
|
|
4186
|
+
trace = Physics::HullTrace.trace_world_and_entities(
|
|
4187
|
+
@level, origin, position, solid_brush, hull_num: 0
|
|
4188
|
+
)
|
|
4189
|
+
trace.fraction >= 1.0
|
|
4190
|
+
end
|
|
4191
|
+
|
|
4192
|
+
def player_damage(amount)
|
|
4193
|
+
multiplier = if @player_state&.powerup_active?(:quad, game_time: @game_time)
|
|
4194
|
+
deathmatch.to_i == 4 ? 8.0 : 4.0
|
|
4195
|
+
else
|
|
4196
|
+
1.0
|
|
4197
|
+
end
|
|
4198
|
+
amount * multiplier
|
|
4199
|
+
end
|
|
4200
|
+
|
|
4201
|
+
def radius_attacker_damage(amount, attacker_player)
|
|
4202
|
+
attacker_player ? player_damage(amount) : amount
|
|
4203
|
+
end
|
|
4204
|
+
|
|
4205
|
+
def projectile_owner_damage(projectile, amount)
|
|
4206
|
+
player_attacker?(projectile.owner) ? player_damage(amount) : amount
|
|
4207
|
+
end
|
|
4208
|
+
|
|
4209
|
+
def projectile_deathtype(projectile)
|
|
4210
|
+
projectile.kind == :super_spike ? "supernail" : "nail"
|
|
4211
|
+
end
|
|
4212
|
+
|
|
4213
|
+
def projectile_radius_deathtype(projectile)
|
|
4214
|
+
case projectile.kind
|
|
4215
|
+
when :rocket then "rocket"
|
|
4216
|
+
when :grenade then "grenade"
|
|
4217
|
+
end
|
|
4218
|
+
end
|
|
4219
|
+
|
|
4220
|
+
def spike_lateral_offset(weapon)
|
|
4221
|
+
return 0.0 if weapon == :super_nailgun
|
|
4222
|
+
|
|
4223
|
+
@spike_lateral_offset ||= 4.0
|
|
4224
|
+
offset = @spike_lateral_offset
|
|
4225
|
+
@spike_lateral_offset = -@spike_lateral_offset
|
|
4226
|
+
offset
|
|
4227
|
+
end
|
|
4228
|
+
|
|
4229
|
+
def crandom
|
|
4230
|
+
2.0 * (rand - 0.5)
|
|
4231
|
+
end
|
|
4232
|
+
|
|
4233
|
+
def angle_forward(angles)
|
|
4234
|
+
yaw = angles.y * ::Math::PI / 180.0
|
|
4235
|
+
pitch = angles.x * ::Math::PI / 180.0
|
|
4236
|
+
cp = ::Math.cos(pitch)
|
|
4237
|
+
Math::Vec3.new(::Math.cos(yaw) * cp, ::Math.sin(yaw) * cp, -::Math.sin(pitch))
|
|
4238
|
+
end
|
|
4239
|
+
|
|
4240
|
+
def damage_entity(ent, amount, attacker: nil, inflictor: nil)
|
|
4241
|
+
if damageable_brush_runtime_entity?(ent) && @brush_game
|
|
4242
|
+
return true if @brush_game.damage_brush_entity(ent, amount, activator: damage_activator(attacker))
|
|
4243
|
+
end
|
|
4244
|
+
|
|
4245
|
+
amount = quake_damage_amount(amount, attacker, inflictor)
|
|
4246
|
+
was_alive = ent.damageable?
|
|
4247
|
+
protected = entity_damage_protected?(ent)
|
|
4248
|
+
old_armor = ent.armorvalue
|
|
4249
|
+
damage = was_alive ? ent.absorb_damage(amount, game_time: @game_time) : { take: 0.0 }
|
|
4250
|
+
taken = damage[:take].to_f
|
|
4251
|
+
armor_saved = old_armor - ent.armorvalue
|
|
4252
|
+
record_client_damage(ent, amount, armor_saved, inflictor) if was_alive
|
|
4253
|
+
apply_damage_momentum(ent, amount, inflictor, attacker) if was_alive
|
|
4254
|
+
return taken if was_alive && teamplay_damage_avoided?(ent, attacker, inflictor)
|
|
4255
|
+
|
|
4256
|
+
ent.apply_health_damage(taken) if was_alive && taken.positive?
|
|
4257
|
+
armor_absorbed_damage = armor_saved.positive?
|
|
4258
|
+
if was_alive && (taken.positive? || (armor_absorbed_damage && !protected))
|
|
4259
|
+
if ent.removed?
|
|
4260
|
+
killed_entity(ent, attacker)
|
|
4261
|
+
else
|
|
4262
|
+
react_to_entity_damage(ent, attacker)
|
|
4263
|
+
call_entity_pain(ent, attacker, taken)
|
|
4264
|
+
end
|
|
4265
|
+
end
|
|
4266
|
+
explode_box(ent) if was_alive && taken.positive? && ent.removed? && explobox?(ent)
|
|
4267
|
+
taken
|
|
4268
|
+
end
|
|
4269
|
+
|
|
4270
|
+
def entity_damage_protected?(ent)
|
|
4271
|
+
return true if (ent.flags & Entity::FL_GODMODE) != 0
|
|
4272
|
+
|
|
4273
|
+
@game_time && ent.invincible_finished.positive? && ent.invincible_finished >= @game_time.to_f
|
|
4274
|
+
end
|
|
4275
|
+
|
|
4276
|
+
def record_client_damage(ent, amount, armor_saved, inflictor)
|
|
4277
|
+
return if (ent.flags & Entity::FL_CLIENT) == 0
|
|
4278
|
+
|
|
4279
|
+
ent.dmg_take += (amount.to_f - armor_saved).ceil
|
|
4280
|
+
ent.dmg_save += armor_saved
|
|
4281
|
+
ent.dmg_inflictor = inflictor
|
|
4282
|
+
end
|
|
4283
|
+
|
|
4284
|
+
def teamplay_damage_avoided?(ent, attacker, inflictor)
|
|
4285
|
+
return false unless [1, 3].include?(teamplay.to_i)
|
|
4286
|
+
return false unless attacker.respond_to?(:classname) && attacker.classname == "player"
|
|
4287
|
+
return false if inflictor.respond_to?(:classname) && inflictor.classname == "door"
|
|
4288
|
+
|
|
4289
|
+
attacker_team = quake_info_key(attacker, "team")
|
|
4290
|
+
return false if attacker_team.empty?
|
|
4291
|
+
return false unless quake_info_key(ent, "team") == attacker_team
|
|
4292
|
+
return false if teamplay.to_i == 3 && ent.equal?(attacker)
|
|
4293
|
+
|
|
4294
|
+
true
|
|
4295
|
+
end
|
|
4296
|
+
|
|
4297
|
+
def quake_info_key(ent, key)
|
|
4298
|
+
return "" unless ent
|
|
4299
|
+
return ent[key].to_s if ent.respond_to?(:[]) && ent[key]
|
|
4300
|
+
return ent.public_send(key).to_s if ent.respond_to?(key)
|
|
4301
|
+
|
|
4302
|
+
value = ent.instance_variable_get(:"@#{key}") if ent.instance_variable_defined?(:"@#{key}")
|
|
4303
|
+
value.to_s
|
|
4304
|
+
end
|
|
4305
|
+
|
|
4306
|
+
def quake_damage_amount(amount, attacker, inflictor)
|
|
4307
|
+
return amount unless attacker_quad_damage_active?(attacker, inflictor)
|
|
4308
|
+
|
|
4309
|
+
amount.to_f * (deathmatch.to_i == 4 ? 8.0 : 4.0)
|
|
4310
|
+
end
|
|
4311
|
+
|
|
4312
|
+
def attacker_quad_damage_active?(attacker, inflictor)
|
|
4313
|
+
return false unless attacker.respond_to?(:super_damage_finished)
|
|
4314
|
+
return false if @game_time.nil?
|
|
4315
|
+
return false if inflictor.respond_to?(:classname) && inflictor.classname == "door"
|
|
4316
|
+
|
|
4317
|
+
attacker.super_damage_finished > @game_time.to_f
|
|
4318
|
+
end
|
|
4319
|
+
|
|
4320
|
+
def damage_activator(attacker)
|
|
4321
|
+
return @player if attacker.nil? || attacker.equal?(@player_state)
|
|
4322
|
+
|
|
4323
|
+
attacker
|
|
4324
|
+
end
|
|
4325
|
+
|
|
4326
|
+
def apply_player_direct_damage_momentum(amount, inflictor)
|
|
4327
|
+
return unless @player&.instance_variable_get(:@movetype) == MOVETYPE_WALK
|
|
4328
|
+
|
|
4329
|
+
apply_player_damage_momentum(amount, inflictor)
|
|
4330
|
+
end
|
|
4331
|
+
|
|
4332
|
+
def apply_player_damage_momentum(amount, inflictor)
|
|
4333
|
+
return unless @player
|
|
4334
|
+
return unless inflictor
|
|
4335
|
+
|
|
4336
|
+
center = inflictor_damage_center(inflictor)
|
|
4337
|
+
return unless center
|
|
4338
|
+
|
|
4339
|
+
dir = (@player.position - center).normalize
|
|
4340
|
+
@player.velocity = @player.velocity + dir * amount.to_f * 8.0
|
|
4341
|
+
# T_Damage adds dir*damage*rj for self-inflicted player damage
|
|
4342
|
+
# (rocket jumps) when the rj server setting exceeds 1
|
|
4343
|
+
return unless rj.to_f > 1.0
|
|
4344
|
+
|
|
4345
|
+
@player.velocity = @player.velocity + dir * amount.to_f * rj.to_f
|
|
4346
|
+
end
|
|
4347
|
+
|
|
4348
|
+
def apply_damage_momentum(ent, amount, inflictor, attacker = nil)
|
|
4349
|
+
return unless inflictor
|
|
4350
|
+
return unless ent.instance_variable_get(:@movetype) == MOVETYPE_WALK
|
|
4351
|
+
|
|
4352
|
+
center = inflictor_damage_center(inflictor)
|
|
4353
|
+
return unless center
|
|
4354
|
+
|
|
4355
|
+
dir = (ent.position - center).normalize
|
|
4356
|
+
ent.velocity = ent.velocity + dir * amount.to_f * 8.0
|
|
4357
|
+
if rocket_jump_modifier_active?(ent, attacker)
|
|
4358
|
+
ent.velocity = ent.velocity + dir * amount.to_f * rj.to_f
|
|
4359
|
+
end
|
|
4360
|
+
end
|
|
4361
|
+
|
|
4362
|
+
def rocket_jump_modifier_active?(ent, attacker)
|
|
4363
|
+
return false unless rj.to_f > 1.0
|
|
4364
|
+
return false unless ent.classname == "player"
|
|
4365
|
+
return false unless attacker.respond_to?(:classname) && attacker.classname == "player"
|
|
4366
|
+
|
|
4367
|
+
quake_info_key(attacker, "netname") == quake_info_key(ent, "netname")
|
|
4368
|
+
end
|
|
4369
|
+
|
|
4370
|
+
def inflictor_damage_center(inflictor)
|
|
4371
|
+
return inflictor if inflictor.is_a?(Math::Vec3)
|
|
4372
|
+
return nil unless inflictor.respond_to?(:position)
|
|
4373
|
+
return inflictor.position + (ENTITY_HULL_MINS + ENTITY_HULL_MAXS) * 0.5 if inflictor.equal?(@player)
|
|
4374
|
+
|
|
4375
|
+
if inflictor.is_a?(Entity)
|
|
4376
|
+
mins, maxs = damageable_bounds(inflictor)
|
|
4377
|
+
return (mins + maxs) * 0.5 if mins && maxs
|
|
4378
|
+
end
|
|
4379
|
+
|
|
4380
|
+
inflictor.position
|
|
4381
|
+
end
|
|
4382
|
+
|
|
4383
|
+
def killed_entity(ent, attacker)
|
|
4384
|
+
movetype = ent.instance_variable_get(:@movetype)
|
|
4385
|
+
if movetype == MOVETYPE_PUSH || movetype == MOVETYPE_NONE
|
|
4386
|
+
call_entity_die(ent)
|
|
4387
|
+
return
|
|
4388
|
+
end
|
|
4389
|
+
|
|
4390
|
+
ent.instance_variable_set(:@enemy, attacker)
|
|
4391
|
+
@killed_monsters = @killed_monsters.to_i + 1 if (ent.flags & Entity::FL_MONSTER) != 0
|
|
4392
|
+
ent.instance_variable_set(:@takedamage, 0)
|
|
4393
|
+
ent.instance_variable_set(:@touch, nil)
|
|
4394
|
+
ent.effects = 0
|
|
4395
|
+
call_entity_die(ent)
|
|
4396
|
+
end
|
|
4397
|
+
|
|
4398
|
+
def react_to_entity_damage(ent, attacker)
|
|
4399
|
+
return unless attacker
|
|
4400
|
+
return unless (ent.flags & Entity::FL_MONSTER) != 0
|
|
4401
|
+
return if ent.equal?(attacker)
|
|
4402
|
+
return if ent.instance_variable_get(:@enemy).equal?(attacker)
|
|
4403
|
+
return if same_monster_class?(ent, attacker) && ent.classname != "monster_army"
|
|
4404
|
+
|
|
4405
|
+
current_enemy = ent.instance_variable_get(:@enemy)
|
|
4406
|
+
ent.instance_variable_set(:@oldenemy, current_enemy) if player_attacker?(current_enemy)
|
|
4407
|
+
ent.instance_variable_set(:@enemy, attacker)
|
|
4408
|
+
ent.instance_variable_set(:@found_target, true)
|
|
4409
|
+
end
|
|
4410
|
+
|
|
4411
|
+
def call_entity_pain(ent, attacker, damage)
|
|
4412
|
+
pain = ent.instance_variable_get(:@th_pain)
|
|
4413
|
+
return unless pain
|
|
4414
|
+
|
|
4415
|
+
ent.instance_variable_set(:@pain_function, pain)
|
|
4416
|
+
ent.instance_variable_set(:@pain_attacker, attacker)
|
|
4417
|
+
ent.instance_variable_set(:@pain_damage, damage.to_f)
|
|
4418
|
+
end
|
|
4419
|
+
|
|
4420
|
+
def call_entity_die(ent)
|
|
4421
|
+
die = ent.instance_variable_get(:@th_die)
|
|
4422
|
+
if die
|
|
4423
|
+
ent.instance_variable_set(:@die_function, die)
|
|
4424
|
+
ent.instance_variable_set(:@die_enemy, ent.instance_variable_get(:@enemy))
|
|
4425
|
+
end
|
|
4426
|
+
end
|
|
4427
|
+
|
|
4428
|
+
def same_monster_class?(ent, attacker)
|
|
4429
|
+
attacker.respond_to?(:classname) && ent.classname == attacker.classname
|
|
4430
|
+
end
|
|
4431
|
+
|
|
4432
|
+
def player_attacker?(attacker)
|
|
4433
|
+
attacker.is_a?(PlayerState) ||
|
|
4434
|
+
(attacker.respond_to?(:classname) && attacker.classname == "player")
|
|
4435
|
+
end
|
|
4436
|
+
|
|
4437
|
+
def trace_damageable_entity(src, dir, max_dist, ignore: [], ignore_solid_trigger: false)
|
|
4438
|
+
best_ent = nil
|
|
4439
|
+
best_t = max_dist
|
|
4440
|
+
|
|
4441
|
+
@entities.each do |ent|
|
|
4442
|
+
next unless ent.damageable?
|
|
4443
|
+
next if ignore.include?(ent)
|
|
4444
|
+
next if ignore_solid_trigger && ent.instance_variable_get(:@solid) == 1
|
|
4445
|
+
|
|
4446
|
+
mins, maxs = damageable_bounds(ent)
|
|
4447
|
+
next unless mins && maxs
|
|
4448
|
+
|
|
4449
|
+
hit_t = ray_aabb(src, dir, mins, maxs)
|
|
4450
|
+
next unless hit_t
|
|
4451
|
+
next if hit_t.negative? || hit_t > best_t
|
|
4452
|
+
|
|
4453
|
+
best_ent = ent
|
|
4454
|
+
best_t = hit_t
|
|
4455
|
+
end
|
|
4456
|
+
|
|
4457
|
+
return nil unless best_ent
|
|
4458
|
+
|
|
4459
|
+
[best_ent, src + dir * best_t]
|
|
4460
|
+
end
|
|
4461
|
+
|
|
4462
|
+
def trace_axe_touch_entity(src, dir, max_dist)
|
|
4463
|
+
trace_touch_entity(src, dir, max_dist)
|
|
4464
|
+
end
|
|
4465
|
+
|
|
4466
|
+
def trace_bullet_touch_entity(src, dir, max_dist)
|
|
4467
|
+
trace_touch_entity(src, dir, max_dist)
|
|
4468
|
+
end
|
|
4469
|
+
|
|
4470
|
+
def trace_lightning_touch_entity(src, dir, max_dist)
|
|
4471
|
+
trace_touch_entity(src, dir, max_dist)
|
|
4472
|
+
end
|
|
4473
|
+
|
|
4474
|
+
def trace_spike_touch_entity(src, dir, max_dist, ignore: nil)
|
|
4475
|
+
trace_touch_entity(src, dir, max_dist, ignore: ignore)
|
|
4476
|
+
end
|
|
4477
|
+
|
|
4478
|
+
def trace_fireball_touch_entity(src, dir, max_dist, ignore: nil)
|
|
4479
|
+
trace_touch_entity(src, dir, max_dist, ignore: ignore)
|
|
4480
|
+
end
|
|
4481
|
+
|
|
4482
|
+
def trace_missile_touch_entity(src, dir, max_dist, ignore: nil)
|
|
4483
|
+
trace_touch_entity(src, dir, max_dist, ignore: ignore)
|
|
4484
|
+
end
|
|
4485
|
+
|
|
4486
|
+
def trace_laser_touch_entity(src, dir, max_dist, ignore: nil)
|
|
4487
|
+
trace_touch_entity(src, dir, max_dist, ignore: ignore)
|
|
4488
|
+
end
|
|
4489
|
+
|
|
4490
|
+
def trace_touch_entity(src, dir, max_dist, ignore: nil)
|
|
4491
|
+
best_ent = nil
|
|
4492
|
+
best_t = max_dist
|
|
4493
|
+
|
|
4494
|
+
@entities.each do |ent|
|
|
4495
|
+
next if ent.removed?
|
|
4496
|
+
next if ent.equal?(ignore)
|
|
4497
|
+
# Traces never collide with SOLID_TRIGGER (1) pickups -- only
|
|
4498
|
+
# damageable or genuinely solid (SOLID_BBOX and up) entities
|
|
4499
|
+
next unless ent.damageable? || ent.instance_variable_get(:@solid).to_i >= SOLID_BBOX
|
|
4500
|
+
|
|
4501
|
+
mins, maxs = damageable_bounds(ent)
|
|
4502
|
+
next unless mins && maxs
|
|
4503
|
+
|
|
4504
|
+
hit_t = ray_aabb(src, dir, mins, maxs)
|
|
4505
|
+
next unless hit_t
|
|
4506
|
+
next if hit_t.negative? || hit_t > best_t
|
|
4507
|
+
|
|
4508
|
+
best_ent = ent
|
|
4509
|
+
best_t = hit_t
|
|
4510
|
+
end
|
|
4511
|
+
|
|
4512
|
+
return nil unless best_ent
|
|
4513
|
+
|
|
4514
|
+
[best_ent, src + dir * best_t]
|
|
4515
|
+
end
|
|
4516
|
+
|
|
4517
|
+
def damageable_bounds(ent)
|
|
4518
|
+
if damageable_brush_runtime_entity?(ent)
|
|
4519
|
+
return nil unless damageable_brush_entity?(ent)
|
|
4520
|
+
|
|
4521
|
+
model = damageable_brush_model(ent)
|
|
4522
|
+
return nil unless model
|
|
4523
|
+
|
|
4524
|
+
[ent.position + model.mins, ent.position + model.maxs]
|
|
4525
|
+
elsif explobox?(ent)
|
|
4526
|
+
height = ent.classname == "misc_explobox2" ? 32.0 : 64.0
|
|
4527
|
+
[ent.position, ent.position + Math::Vec3.new(32.0, 32.0, height)]
|
|
4528
|
+
else
|
|
4529
|
+
[ent.position + ENTITY_HULL_MINS, ent.position + ENTITY_HULL_MAXS]
|
|
4530
|
+
end
|
|
4531
|
+
end
|
|
4532
|
+
|
|
4533
|
+
def damageable_position(ent)
|
|
4534
|
+
if damageable_brush_runtime_entity?(ent) && damageable_brush_entity?(ent)
|
|
4535
|
+
model = damageable_brush_model(ent)
|
|
4536
|
+
return ent.position + (model.mins + model.maxs) * 0.5 if model
|
|
4537
|
+
end
|
|
4538
|
+
|
|
4539
|
+
ent.position
|
|
4540
|
+
end
|
|
4541
|
+
|
|
4542
|
+
def radius_damage_center(ent)
|
|
4543
|
+
mins, maxs = damageable_bounds(ent)
|
|
4544
|
+
return damageable_position(ent) unless mins && maxs
|
|
4545
|
+
|
|
4546
|
+
(mins + maxs) * 0.5
|
|
4547
|
+
end
|
|
4548
|
+
|
|
4549
|
+
def damageable_brush_entity?(ent)
|
|
4550
|
+
ent.classname.start_with?("trigger_") ||
|
|
4551
|
+
ent.classname == "func_button" ||
|
|
4552
|
+
ent.classname == "func_door" ||
|
|
4553
|
+
ent.instance_variable_get(:@regular_door) ||
|
|
4554
|
+
ent.instance_variable_get(:@secret_door)
|
|
4555
|
+
end
|
|
4556
|
+
|
|
4557
|
+
def damageable_brush_runtime_entity?(ent)
|
|
4558
|
+
ent.brush_entity? || !ent.instance_variable_get(:@trigger_model_index).nil?
|
|
4559
|
+
end
|
|
4560
|
+
|
|
4561
|
+
def damageable_brush_model(ent)
|
|
4562
|
+
model_index = ent.model_index || ent.instance_variable_get(:@trigger_model_index)
|
|
4563
|
+
@level.models[model_index] if model_index && @level
|
|
4564
|
+
end
|
|
4565
|
+
|
|
4566
|
+
def explobox?(ent)
|
|
4567
|
+
ent.classname == "misc_explobox" || ent.classname == "misc_explobox2"
|
|
4568
|
+
end
|
|
4569
|
+
|
|
4570
|
+
def explode_box(ent)
|
|
4571
|
+
ent.instance_variable_set(:@takedamage, 0)
|
|
4572
|
+
ent.classname = "explo_box"
|
|
4573
|
+
apply_radius_damage(ent.position, 160.0, ignore: ent, attacker_player: false, attacker: ent, inflictor: ent)
|
|
4574
|
+
play_temp_explosion_sound
|
|
4575
|
+
effect_position = ent.position + Math::Vec3.new(0.0, 0.0, 32.0)
|
|
4576
|
+
add_temp_explosion_dynamic_light(effect_position)
|
|
4577
|
+
@particles&.explosion(effect_position)
|
|
4578
|
+
end
|
|
4579
|
+
|
|
4580
|
+
def play_temp_explosion_sound
|
|
4581
|
+
@sound_events&.on_weapon_fire(WeaponSound.new(sound: "weapons/r_exp3.wav"))
|
|
4582
|
+
end
|
|
4583
|
+
|
|
4584
|
+
def ray_aabb(origin, dir, mins, maxs)
|
|
4585
|
+
t_min = 0.0
|
|
4586
|
+
t_max = Float::INFINITY
|
|
4587
|
+
|
|
4588
|
+
[:x, :y, :z].each do |axis|
|
|
4589
|
+
o = origin.public_send(axis)
|
|
4590
|
+
d = dir.public_send(axis)
|
|
4591
|
+
min = mins.public_send(axis)
|
|
4592
|
+
max = maxs.public_send(axis)
|
|
4593
|
+
|
|
4594
|
+
if d.abs < 0.000001
|
|
4595
|
+
return nil if o < min || o > max
|
|
4596
|
+
next
|
|
4597
|
+
end
|
|
4598
|
+
|
|
4599
|
+
inv = 1.0 / d
|
|
4600
|
+
t1 = (min - o) * inv
|
|
4601
|
+
t2 = (max - o) * inv
|
|
4602
|
+
t1, t2 = t2, t1 if t1 > t2
|
|
4603
|
+
|
|
4604
|
+
t_min = [t_min, t1].max
|
|
4605
|
+
t_max = [t_max, t2].min
|
|
4606
|
+
return nil if t_min > t_max
|
|
4607
|
+
end
|
|
4608
|
+
|
|
4609
|
+
t_min
|
|
4610
|
+
end
|
|
4611
|
+
|
|
4612
|
+
def aabb_intersect?(mins_a, maxs_a, mins_b, maxs_b)
|
|
4613
|
+
mins_a.x <= maxs_b.x && maxs_a.x >= mins_b.x &&
|
|
4614
|
+
mins_a.y <= maxs_b.y && maxs_a.y >= mins_b.y &&
|
|
4615
|
+
mins_a.z <= maxs_b.z && maxs_a.z >= mins_b.z
|
|
4616
|
+
end
|
|
298
4617
|
end
|
|
299
4618
|
end
|
|
300
4619
|
end
|