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
|
@@ -5,42 +5,75 @@ module Quake
|
|
|
5
5
|
# Manages brush entity behavior: doors, platforms, buttons, triggers.
|
|
6
6
|
# Each entity has a state machine and moves between positions.
|
|
7
7
|
class BrushEntities
|
|
8
|
+
BUTTON_TOUCH_RADIUS = 40.0
|
|
8
9
|
# Entity states
|
|
9
10
|
STATE_IDLE = :idle
|
|
10
11
|
STATE_OPENING = :opening
|
|
11
12
|
STATE_OPEN = :open
|
|
12
13
|
STATE_CLOSING = :closing
|
|
13
14
|
STATE_CLOSED = :closed
|
|
15
|
+
PLAT_LOW_TRIGGER = 1
|
|
16
|
+
FL_FLY = 1
|
|
17
|
+
FL_SWIM = 2
|
|
18
|
+
FL_MONSTER = 32
|
|
19
|
+
FL_ONGROUND = 512
|
|
14
20
|
|
|
15
|
-
|
|
21
|
+
attr_reader :total_secrets, :found_secrets
|
|
22
|
+
|
|
23
|
+
def initialize(entities, level, target_map, registered: false, sound_events: nil, serverflags: 0, worldtype: 0)
|
|
16
24
|
@entities = entities
|
|
17
25
|
@level = level
|
|
18
26
|
@target_map = target_map
|
|
19
|
-
@
|
|
27
|
+
@registered = registered
|
|
28
|
+
@sound_events = sound_events
|
|
29
|
+
@serverflags = serverflags.to_i
|
|
30
|
+
@worldtype = worldtype.to_i
|
|
31
|
+
@time = 0.0
|
|
32
|
+
@total_secrets = 0
|
|
33
|
+
@found_secrets = 0
|
|
34
|
+
@brush_entities = entities.select { |ent| ent.brush_entity? || ent.classname == "misc_teleporttrain" }
|
|
35
|
+
@delayed_uses = []
|
|
20
36
|
|
|
37
|
+
@entities.each { |ent| init_point_trigger(ent) }
|
|
21
38
|
# Initialize movement data for each brush entity
|
|
22
39
|
@brush_entities.each { |ent| init_brush_entity(ent) }
|
|
40
|
+
link_doors
|
|
23
41
|
|
|
24
42
|
puts "Initialized #{@brush_entities.size} brush entities"
|
|
25
43
|
end
|
|
26
44
|
|
|
27
45
|
# Update all brush entities. Returns entities that need rendering.
|
|
28
|
-
def update(dt, player_pos)
|
|
46
|
+
def update(dt, player_pos, player_state: nil)
|
|
47
|
+
@time_step = dt
|
|
48
|
+
@time += dt
|
|
49
|
+
update_delayed_uses(dt)
|
|
50
|
+
@entities.each { |ent| update_scheduled_remove(ent, dt) }
|
|
51
|
+
|
|
29
52
|
# Save previous positions for platform-riding
|
|
30
53
|
@brush_entities.each do |ent|
|
|
31
54
|
ent.instance_variable_set(:@prev_pos, ent.position)
|
|
32
55
|
end
|
|
33
56
|
|
|
34
57
|
@brush_entities.each do |ent|
|
|
58
|
+
next if ent.instance_variable_get(:@removed)
|
|
59
|
+
|
|
60
|
+
update_trigger_cooldown(ent, dt)
|
|
61
|
+
|
|
35
62
|
case ent.classname
|
|
36
|
-
when "func_door"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
63
|
+
when "func_door", "door"
|
|
64
|
+
if secret_door?(ent)
|
|
65
|
+
update_secret_door(ent, player_pos, player_state)
|
|
66
|
+
else
|
|
67
|
+
update_door(ent, dt, player_pos, player_state)
|
|
68
|
+
end
|
|
69
|
+
when "func_plat", "plat"
|
|
70
|
+
update_platform(ent, dt, player_pos, player_state)
|
|
40
71
|
when "func_button"
|
|
41
|
-
update_button(ent, dt, player_pos)
|
|
42
|
-
when "func_train"
|
|
43
|
-
update_train(ent, dt)
|
|
72
|
+
update_button(ent, dt, player_pos, player_state)
|
|
73
|
+
when "func_train", "train", "misc_teleporttrain"
|
|
74
|
+
update_train(ent, dt, player_pos, player_state)
|
|
75
|
+
when "func_door_secret"
|
|
76
|
+
update_secret_door(ent, player_pos, player_state)
|
|
44
77
|
end
|
|
45
78
|
end
|
|
46
79
|
|
|
@@ -51,9 +84,11 @@ module Quake
|
|
|
51
84
|
# position snapped to the platform's top surface (so on_ground stays
|
|
52
85
|
# true and the player rides smoothly). Returns nil if not on a
|
|
53
86
|
# moving platform.
|
|
87
|
+
VERTICAL_RIDER_CLASSNAMES = %w[func_plat plat func_door door].freeze
|
|
88
|
+
|
|
54
89
|
def snap_to_platform(player_pos)
|
|
55
90
|
@brush_entities.each do |ent|
|
|
56
|
-
next unless ent.classname
|
|
91
|
+
next unless VERTICAL_RIDER_CLASSNAMES.include?(ent.classname)
|
|
57
92
|
model = @level.models[ent.model_index]
|
|
58
93
|
next unless model
|
|
59
94
|
|
|
@@ -61,21 +96,25 @@ module Quake
|
|
|
61
96
|
next unless prev
|
|
62
97
|
|
|
63
98
|
ent_delta_z = ent.position.z - prev.z
|
|
99
|
+
# Doors join the rider check only while actually moving
|
|
100
|
+
# vertically (elevators like e1m1's t1 are vertical func_doors --
|
|
101
|
+
# SV_PushMove carries riders for every pusher, not just plats).
|
|
102
|
+
next if ent.classname.include?("door") && ent_delta_z.zero?
|
|
64
103
|
|
|
65
104
|
# Use the OLD platform position for the on_top check, since
|
|
66
105
|
# player_pos hasn't been updated for this frame's movement yet.
|
|
106
|
+
# The player origin rides 24 above the surface (hull mins.z -24).
|
|
67
107
|
old_top_z = prev.z + model.maxs.z
|
|
68
|
-
next unless player_pos.z >= old_top_z
|
|
108
|
+
next unless player_pos.z >= old_top_z + 16 && player_pos.z <= old_top_z + 48
|
|
69
109
|
next unless player_pos.x >= ent.position.x + model.mins.x &&
|
|
70
110
|
player_pos.x <= ent.position.x + model.maxs.x &&
|
|
71
111
|
player_pos.y >= ent.position.y + model.mins.y &&
|
|
72
112
|
player_pos.y <= ent.position.y + model.maxs.y
|
|
73
113
|
|
|
74
|
-
# Snap to the new
|
|
75
|
-
# trace-down ground check
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return Math::Vec3.new(player_pos.x, player_pos.y, new_top_z)
|
|
114
|
+
# Snap the origin to 24 above the new top surface (feet slightly
|
|
115
|
+
# above it so the trace-down ground check detects it cleanly).
|
|
116
|
+
new_top_z = ent.position.z + model.maxs.z
|
|
117
|
+
return Math::Vec3.new(player_pos.x, player_pos.y, new_top_z + 24.5)
|
|
79
118
|
end
|
|
80
119
|
nil
|
|
81
120
|
end
|
|
@@ -88,12 +127,18 @@ module Quake
|
|
|
88
127
|
end
|
|
89
128
|
|
|
90
129
|
# Check if player touches any trigger entities
|
|
91
|
-
def check_triggers(player_pos, player_radius: 16.0)
|
|
130
|
+
def check_triggers(player_pos, player_radius: 16.0, player_forward: nil)
|
|
92
131
|
@entities.each do |ent|
|
|
93
132
|
next unless ent.classname.start_with?("trigger_")
|
|
94
|
-
next unless ent
|
|
133
|
+
next unless trigger_model_index(ent)
|
|
134
|
+
next if ent.instance_variable_get(:@removed)
|
|
135
|
+
next if ent.instance_variable_get(:@touch_disabled)
|
|
136
|
+
next if notouch_trigger?(ent)
|
|
137
|
+
next if health_trigger?(ent)
|
|
138
|
+
next unless trigger_facing_allowed?(ent, player_forward)
|
|
139
|
+
next if trigger_cooldown_active?(ent)
|
|
95
140
|
|
|
96
|
-
model = @level.models[ent
|
|
141
|
+
model = @level.models[trigger_model_index(ent)]
|
|
97
142
|
next unless model
|
|
98
143
|
|
|
99
144
|
# Simple AABB check against player
|
|
@@ -107,40 +152,292 @@ module Quake
|
|
|
107
152
|
player_pos.y <= origin.y + maxs.y + player_radius &&
|
|
108
153
|
player_pos.z >= origin.z + mins.z - player_radius &&
|
|
109
154
|
player_pos.z <= origin.z + maxs.z + player_radius
|
|
110
|
-
fire_targets(ent.target) if ent.target
|
|
111
155
|
yield ent if block_given?
|
|
112
156
|
end
|
|
113
157
|
end
|
|
114
158
|
end
|
|
115
159
|
|
|
160
|
+
# QuakeC SUB_UseTargets, limited to local entity state transitions.
|
|
161
|
+
# The engine owns side effects like changelevel and teleport movement.
|
|
162
|
+
def use_targets(source, activator: nil)
|
|
163
|
+
delay = (source["delay"] || "0").to_f
|
|
164
|
+
unless delay.zero?
|
|
165
|
+
@delayed_uses << {
|
|
166
|
+
remaining: delay,
|
|
167
|
+
message: source.message,
|
|
168
|
+
killtarget: source.killtarget,
|
|
169
|
+
target: source.target,
|
|
170
|
+
activator: activator || source
|
|
171
|
+
}
|
|
172
|
+
return
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
fire_use_targets(
|
|
176
|
+
source.killtarget,
|
|
177
|
+
source.target,
|
|
178
|
+
activator: activator || source,
|
|
179
|
+
message: source.message,
|
|
180
|
+
source_noise: source.instance_variable_get(:@noise)
|
|
181
|
+
)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def touch_trigger(ent, activator: nil)
|
|
185
|
+
return false if ent.instance_variable_get(:@touch_disabled)
|
|
186
|
+
return false if trigger_cooldown_active?(ent)
|
|
187
|
+
|
|
188
|
+
case ent.classname
|
|
189
|
+
when "trigger_once"
|
|
190
|
+
return false unless activator&.classname == "player"
|
|
191
|
+
return false unless trigger_touch_facing_allowed?(ent, activator)
|
|
192
|
+
|
|
193
|
+
activate_multi_trigger(ent, activator: activator)
|
|
194
|
+
when "trigger_secret"
|
|
195
|
+
return false unless trigger_touch_facing_allowed?(ent, activator)
|
|
196
|
+
|
|
197
|
+
touch_secret_trigger(ent, activator: activator)
|
|
198
|
+
when "trigger_multiple"
|
|
199
|
+
return false unless activator&.classname == "player"
|
|
200
|
+
return false unless trigger_touch_facing_allowed?(ent, activator)
|
|
201
|
+
|
|
202
|
+
activate_multi_trigger(ent, activator: activator)
|
|
203
|
+
when "trigger_onlyregistered"
|
|
204
|
+
touch_onlyregistered(ent, activator: activator)
|
|
205
|
+
when "trigger_monsterjump"
|
|
206
|
+
touch_monsterjump(ent, activator)
|
|
207
|
+
when "trigger_push"
|
|
208
|
+
touch_push(ent, activator)
|
|
209
|
+
when "trigger_hurt"
|
|
210
|
+
touch_hurt(ent, activator)
|
|
211
|
+
else
|
|
212
|
+
false
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def damage_trigger(ent, amount, activator: nil)
|
|
217
|
+
damage_brush_entity(ent, amount, activator: activator)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def damage_brush_entity(ent, amount, activator: nil)
|
|
221
|
+
return damage_button(ent, amount, activator: activator) if ent.classname == "func_button"
|
|
222
|
+
return damage_door(ent, amount, activator: activator) if regular_door?(ent)
|
|
223
|
+
return damage_secret_door(ent, activator: activator) if secret_door?(ent)
|
|
224
|
+
return false unless ["trigger_once", "trigger_multiple", "trigger_secret"].include?(ent.classname)
|
|
225
|
+
return false unless ent.health.positive?
|
|
226
|
+
|
|
227
|
+
ent.instance_variable_set(:@max_health, ent.health) unless ent.instance_variable_get(:@max_health)
|
|
228
|
+
ent.health -= amount.to_f.ceil
|
|
229
|
+
return true if ent.health.positive?
|
|
230
|
+
|
|
231
|
+
ent.health = -99.0 if ent.health < -99.0
|
|
232
|
+
activate_multi_trigger(ent, activator: activator || ent)
|
|
233
|
+
true
|
|
234
|
+
end
|
|
235
|
+
|
|
116
236
|
private
|
|
117
237
|
|
|
238
|
+
def activate_multi_trigger(ent, activator:)
|
|
239
|
+
return false if multi_trigger_waiting?(ent)
|
|
240
|
+
return touch_secret_trigger(ent, activator: activator) if ent.classname == "trigger_secret"
|
|
241
|
+
|
|
242
|
+
ent.instance_variable_set(:@takedamage, 0)
|
|
243
|
+
play_trigger_sound(ent)
|
|
244
|
+
use_targets(ent, activator: activator)
|
|
245
|
+
if ent.wait.negative?
|
|
246
|
+
ent.instance_variable_set(:@touch_disabled, true)
|
|
247
|
+
schedule_remove(ent, 0.1)
|
|
248
|
+
else
|
|
249
|
+
ent.instance_variable_set(:@think, :multi_wait)
|
|
250
|
+
ent.think_time = ent.wait.positive? ? ent.wait : 0.2
|
|
251
|
+
end
|
|
252
|
+
true
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def init_point_trigger(ent)
|
|
256
|
+
case ent.classname
|
|
257
|
+
when "trigger_relay"
|
|
258
|
+
ent.instance_variable_set(:@use, :SUB_UseTargets)
|
|
259
|
+
when "trigger_counter"
|
|
260
|
+
ent.wait = -1.0
|
|
261
|
+
ent.count = 2 if ent.count.zero?
|
|
262
|
+
ent.instance_variable_set(:@use, :counter_use)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
118
266
|
def init_brush_entity(ent)
|
|
267
|
+
if ent.classname == "misc_teleporttrain"
|
|
268
|
+
init_moving_entity(ent, Math::Vec3::ORIGIN)
|
|
269
|
+
setup_teleport_train(ent)
|
|
270
|
+
return
|
|
271
|
+
end
|
|
272
|
+
|
|
119
273
|
return unless ent.model_index
|
|
120
274
|
|
|
121
275
|
model = @level.models[ent.model_index]
|
|
122
276
|
return unless model
|
|
123
277
|
|
|
124
|
-
|
|
125
|
-
ent.instance_variable_set(:@
|
|
126
|
-
ent.instance_variable_set(:@
|
|
127
|
-
ent.instance_variable_set(:@move_fraction, 0.0)
|
|
278
|
+
init_moving_entity(ent, model.mins)
|
|
279
|
+
ent.instance_variable_set(:@trigger_model_index, ent.model_index) if init_trigger_class?(ent)
|
|
280
|
+
ent.instance_variable_set(:@max_health, ent.health) if health_trigger?(ent)
|
|
128
281
|
|
|
129
282
|
case ent.classname
|
|
283
|
+
when "trigger_teleport"
|
|
284
|
+
setup_teleport_trigger(ent)
|
|
285
|
+
when "trigger_changelevel"
|
|
286
|
+
setup_changelevel_trigger(ent)
|
|
287
|
+
when "trigger_hurt"
|
|
288
|
+
setup_hurt_trigger(ent)
|
|
289
|
+
when "trigger_onlyregistered"
|
|
290
|
+
setup_onlyregistered_trigger(ent)
|
|
291
|
+
when "trigger_setskill"
|
|
292
|
+
remove_entity(ent)
|
|
293
|
+
when "trigger_once", "trigger_multiple"
|
|
294
|
+
setup_multi_trigger(ent)
|
|
295
|
+
when "trigger_secret"
|
|
296
|
+
setup_secret_trigger(ent)
|
|
297
|
+
when "trigger_push"
|
|
298
|
+
setup_push_trigger(ent)
|
|
299
|
+
when "trigger_monsterjump"
|
|
300
|
+
setup_monsterjump_trigger(ent)
|
|
130
301
|
when "func_door"
|
|
131
302
|
setup_door(ent, model)
|
|
303
|
+
when "func_door_secret"
|
|
304
|
+
setup_secret_door(ent, model)
|
|
132
305
|
when "func_plat"
|
|
133
306
|
setup_platform(ent, model)
|
|
134
307
|
when "func_button"
|
|
135
308
|
setup_button(ent, model)
|
|
309
|
+
when "func_train"
|
|
310
|
+
setup_train(ent, model)
|
|
311
|
+
when "func_wall", "func_illusionary"
|
|
312
|
+
setup_static_brush_model(ent)
|
|
313
|
+
when "func_episodegate"
|
|
314
|
+
setup_episode_gate(ent)
|
|
315
|
+
when "func_bossgate"
|
|
316
|
+
setup_boss_gate(ent)
|
|
136
317
|
end
|
|
137
318
|
end
|
|
138
319
|
|
|
320
|
+
def setup_teleport_trigger(ent)
|
|
321
|
+
setup_init_trigger(ent)
|
|
322
|
+
ent.instance_variable_set(:@touch, :teleport_touch)
|
|
323
|
+
ent.instance_variable_set(:@use, :teleport_use)
|
|
324
|
+
raise "no target" if ent.target.nil? || ent.target.empty?
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def setup_changelevel_trigger(ent)
|
|
328
|
+
setup_init_trigger(ent)
|
|
329
|
+
ent.instance_variable_set(:@touch, :changelevel_touch)
|
|
330
|
+
raise "chagnelevel trigger doesn't have map" if ent["map"].nil? || ent["map"].empty?
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def setup_hurt_trigger(ent)
|
|
334
|
+
setup_init_trigger(ent)
|
|
335
|
+
ent.instance_variable_set(:@touch, :hurt_touch)
|
|
336
|
+
ent.properties["dmg"] = "5" if ent["dmg"].nil? || ent["dmg"].to_f.zero?
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def setup_onlyregistered_trigger(ent)
|
|
340
|
+
setup_init_trigger(ent)
|
|
341
|
+
ent.instance_variable_set(:@touch, :trigger_onlyregistered_touch)
|
|
342
|
+
ent.instance_variable_set(:@noise, "misc/talk.wav")
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def init_moving_entity(ent, mins)
|
|
346
|
+
ent.instance_variable_set(:@closed_pos, ent.position)
|
|
347
|
+
ent.instance_variable_set(:@open_pos, ent.position)
|
|
348
|
+
ent.instance_variable_set(:@move_fraction, 0.0)
|
|
349
|
+
ent.instance_variable_set(:@train_mins, mins)
|
|
350
|
+
end
|
|
351
|
+
|
|
139
352
|
DOOR_START_OPEN = 1
|
|
353
|
+
DOOR_DONT_LINK = 4
|
|
354
|
+
DOOR_GOLD_KEY = 8
|
|
355
|
+
DOOR_SILVER_KEY = 16
|
|
356
|
+
DOOR_TOGGLE = 32
|
|
357
|
+
SECRET_NO_SHOOT = 8
|
|
358
|
+
SECRET_YES_SHOOT = 16
|
|
359
|
+
|
|
360
|
+
def setup_secret_trigger(ent)
|
|
361
|
+
@total_secrets += 1
|
|
362
|
+
ent.wait = -1.0
|
|
363
|
+
ent.message ||= "You found a secret area!"
|
|
364
|
+
ent.sounds = 1 if ent.sounds.zero?
|
|
365
|
+
case ent.sounds
|
|
366
|
+
when 1
|
|
367
|
+
ent.instance_variable_set(:@noise, "misc/secret.wav")
|
|
368
|
+
when 2
|
|
369
|
+
ent.instance_variable_set(:@noise, "misc/talk.wav")
|
|
370
|
+
end
|
|
371
|
+
setup_multi_trigger(ent)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def setup_multi_trigger(ent)
|
|
375
|
+
setup_init_trigger(ent)
|
|
376
|
+
ent.instance_variable_set(:@use, :multi_use)
|
|
377
|
+
if ent.health.positive? && (ent.spawnflags & 1) != 0
|
|
378
|
+
raise "health and notouch don't make sense"
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
case ent.sounds
|
|
382
|
+
when 1
|
|
383
|
+
ent.instance_variable_set(:@noise, "misc/secret.wav")
|
|
384
|
+
when 2
|
|
385
|
+
ent.instance_variable_set(:@noise, "misc/talk.wav")
|
|
386
|
+
when 3
|
|
387
|
+
ent.instance_variable_set(:@noise, "misc/trigger1.wav")
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
unless ent.health.positive?
|
|
391
|
+
ent.instance_variable_set(:@touch, :multi_touch) if (ent.spawnflags & 1).zero?
|
|
392
|
+
return
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
ent.instance_variable_set(:@takedamage, 1)
|
|
396
|
+
ent.instance_variable_set(:@solid, 2)
|
|
397
|
+
ent.instance_variable_set(:@max_health, ent.health)
|
|
398
|
+
ent.instance_variable_set(:@th_die, :multi_killed)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def setup_push_trigger(ent)
|
|
402
|
+
setup_init_trigger(ent)
|
|
403
|
+
ent.instance_variable_set(:@touch, :trigger_push_touch)
|
|
404
|
+
ent.speed = 1000.0 if ent.speed.zero?
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def setup_monsterjump_trigger(ent)
|
|
408
|
+
ent.speed = 200.0 if ent.speed.zero?
|
|
409
|
+
ent.height = 200.0 if ent.height.zero?
|
|
410
|
+
if ent.angles == Math::Vec3::ORIGIN && (!ent["angle"] || ent["angle"].to_f.zero?)
|
|
411
|
+
ent.angles = Math::Vec3.new(0.0, 360.0, 0.0)
|
|
412
|
+
end
|
|
413
|
+
setup_init_trigger(ent)
|
|
414
|
+
ent.instance_variable_set(:@touch, :trigger_monsterjump_touch)
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def setup_init_trigger(ent)
|
|
418
|
+
if ent.angles != Math::Vec3::ORIGIN
|
|
419
|
+
ent.move_dir = ent.forward_vector
|
|
420
|
+
ent.angles = Math::Vec3::ORIGIN
|
|
421
|
+
end
|
|
422
|
+
ent.instance_variable_set(:@solid, 1)
|
|
423
|
+
ent.instance_variable_set(:@movetype, 0)
|
|
424
|
+
ent.properties["model"] = ""
|
|
425
|
+
ent.model_index = nil
|
|
426
|
+
end
|
|
140
427
|
|
|
141
428
|
def setup_door(ent, model)
|
|
142
|
-
|
|
429
|
+
setup_bsp_push(ent)
|
|
430
|
+
ent.instance_variable_set(:@blocked, :door_blocked)
|
|
431
|
+
ent.instance_variable_set(:@use, :door_use)
|
|
432
|
+
ent.instance_variable_set(:@touch, :door_touch)
|
|
433
|
+
ent.instance_variable_set(:@think, :LinkDoors)
|
|
434
|
+
ent.instance_variable_set(:@nextthink, @time + 0.1)
|
|
435
|
+
ent.properties["dmg"] = "2" if ent["dmg"].nil? || ent["dmg"].to_f.zero?
|
|
436
|
+
# QuakeC SetMovedir consumes the authored angle and clears it.
|
|
143
437
|
dir = ent.forward_vector
|
|
438
|
+
ent.move_dir = dir
|
|
439
|
+
ent.angle = 0.0
|
|
440
|
+
ent.angles = Math::Vec3::ORIGIN
|
|
144
441
|
size = model.maxs - model.mins
|
|
145
442
|
move_dist = dir.x.abs * size.x + dir.y.abs * size.y + dir.z.abs * size.z
|
|
146
443
|
move_dist -= ent.lip
|
|
@@ -159,9 +456,106 @@ module Quake
|
|
|
159
456
|
ent.instance_variable_set(:@closed_pos, pos1)
|
|
160
457
|
ent.instance_variable_set(:@open_pos, pos2)
|
|
161
458
|
end
|
|
459
|
+
ent.instance_variable_set(:@max_health, ent.health) if ent.health.positive?
|
|
460
|
+
if ent.health.positive?
|
|
461
|
+
ent.instance_variable_set(:@takedamage, 1)
|
|
462
|
+
ent.instance_variable_set(:@th_die, :door_killed)
|
|
463
|
+
end
|
|
464
|
+
ent.instance_variable_set(:@regular_door, true)
|
|
465
|
+
apply_door_noise(ent)
|
|
466
|
+
apply_key_door_noise(ent)
|
|
467
|
+
ent.classname = "door"
|
|
468
|
+
|
|
469
|
+
if (spawnflags & DOOR_SILVER_KEY) != 0
|
|
470
|
+
ent.instance_variable_set(:@required_key, :silver)
|
|
471
|
+
ent.wait = -1.0
|
|
472
|
+
end
|
|
473
|
+
if (spawnflags & DOOR_GOLD_KEY) != 0
|
|
474
|
+
ent.instance_variable_set(:@required_key, :gold)
|
|
475
|
+
ent.wait = -1.0
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def apply_door_noise(ent)
|
|
480
|
+
noise1, noise2 = case ent.sounds
|
|
481
|
+
when 1
|
|
482
|
+
["doors/drclos4.wav", "doors/doormv1.wav"]
|
|
483
|
+
when 2
|
|
484
|
+
["doors/hydro2.wav", "doors/hydro1.wav"]
|
|
485
|
+
when 3
|
|
486
|
+
["doors/stndr2.wav", "doors/stndr1.wav"]
|
|
487
|
+
when 4
|
|
488
|
+
["doors/ddoor2.wav", "doors/ddoor1.wav"]
|
|
489
|
+
else
|
|
490
|
+
["misc/null.wav", "misc/null.wav"]
|
|
491
|
+
end
|
|
492
|
+
ent.instance_variable_set(:@noise1, noise1)
|
|
493
|
+
ent.instance_variable_set(:@noise2, noise2)
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def apply_key_door_noise(ent)
|
|
497
|
+
noise3, noise4 = case @worldtype
|
|
498
|
+
when 1
|
|
499
|
+
["doors/runetry.wav", "doors/runeuse.wav"]
|
|
500
|
+
when 2
|
|
501
|
+
["doors/basetry.wav", "doors/baseuse.wav"]
|
|
502
|
+
else
|
|
503
|
+
["doors/medtry.wav", "doors/meduse.wav"]
|
|
504
|
+
end
|
|
505
|
+
ent.instance_variable_set(:@noise3, noise3)
|
|
506
|
+
ent.instance_variable_set(:@noise4, noise4)
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def setup_secret_door(ent, model)
|
|
510
|
+
setup_bsp_push(ent)
|
|
511
|
+
ent.sounds = 3 if ent.sounds.zero?
|
|
512
|
+
apply_secret_door_noise(ent)
|
|
513
|
+
ent.speed = 50.0
|
|
514
|
+
ent.wait = 5.0 unless ent["wait"] && ent.wait.nonzero?
|
|
515
|
+
ent.properties["dmg"] = "2" if ent["dmg"].nil? || ent["dmg"].to_f.zero?
|
|
516
|
+
ent.instance_variable_set(:@dmg, ent["dmg"].to_f)
|
|
517
|
+
ent.instance_variable_set(:@oldorigin, ent.position)
|
|
518
|
+
ent.instance_variable_set(:@mangle, ent.angles)
|
|
519
|
+
ent.angles = Math::Vec3::ORIGIN
|
|
520
|
+
ent.instance_variable_set(:@touch, :secret_touch)
|
|
521
|
+
ent.instance_variable_set(:@blocked, :secret_blocked)
|
|
522
|
+
ent.instance_variable_set(:@use, :fd_secret_use)
|
|
523
|
+
ent.instance_variable_set(:@secret_door, true)
|
|
524
|
+
ent.instance_variable_set(:@secret_size, model.maxs - model.mins)
|
|
525
|
+
ent.classname = "door"
|
|
526
|
+
return unless secret_door_shootable?(ent)
|
|
527
|
+
|
|
528
|
+
ent.health = 10_000.0
|
|
529
|
+
ent.instance_variable_set(:@max_health, 10_000.0)
|
|
530
|
+
ent.instance_variable_set(:@takedamage, 1)
|
|
531
|
+
ent.instance_variable_set(:@th_pain, :fd_secret_use)
|
|
532
|
+
ent.instance_variable_set(:@th_die, :fd_secret_use)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def apply_secret_door_noise(ent)
|
|
536
|
+
noise1, noise2, noise3 = case ent.sounds
|
|
537
|
+
when 1
|
|
538
|
+
["doors/latch2.wav", "doors/winch2.wav", "doors/drclos4.wav"]
|
|
539
|
+
when 2
|
|
540
|
+
["doors/airdoor2.wav", "doors/airdoor1.wav", "doors/airdoor2.wav"]
|
|
541
|
+
else
|
|
542
|
+
["doors/basesec2.wav", "doors/basesec1.wav", "doors/basesec2.wav"]
|
|
543
|
+
end
|
|
544
|
+
ent.instance_variable_set(:@noise1, noise1)
|
|
545
|
+
ent.instance_variable_set(:@noise2, noise2)
|
|
546
|
+
ent.instance_variable_set(:@noise3, noise3)
|
|
162
547
|
end
|
|
163
548
|
|
|
164
549
|
def setup_platform(ent, model)
|
|
550
|
+
setup_bsp_push(ent)
|
|
551
|
+
ent.sounds = 2 if ent.sounds.zero?
|
|
552
|
+
apply_platform_noise(ent)
|
|
553
|
+
ent.wait = 3.0
|
|
554
|
+
ent.instance_variable_set(:@mangle, ent.angles)
|
|
555
|
+
ent.angles = Math::Vec3::ORIGIN
|
|
556
|
+
ent.instance_variable_set(:@blocked, :plat_crush)
|
|
557
|
+
ent.classname = "plat"
|
|
558
|
+
|
|
165
559
|
# In Quake, the map origin is the TOP position.
|
|
166
560
|
# See plats.qc func_plat:
|
|
167
561
|
# pos1 = top (authored origin)
|
|
@@ -170,7 +564,7 @@ module Quake
|
|
|
170
564
|
# the player steps on them. Platforms WITH a targetname start at
|
|
171
565
|
# the top and go down when triggered.
|
|
172
566
|
size = model.maxs - model.mins
|
|
173
|
-
height = size.z - 8.0
|
|
567
|
+
height = ent["height"].to_f.positive? ? ent["height"].to_f : (size.z - 8.0)
|
|
174
568
|
|
|
175
569
|
top_pos = ent.position
|
|
176
570
|
bottom_pos = Math::Vec3.new(top_pos.x, top_pos.y, top_pos.z - height)
|
|
@@ -179,108 +573,413 @@ module Quake
|
|
|
179
573
|
# Targeted: start at top, button press lowers it
|
|
180
574
|
ent.instance_variable_set(:@closed_pos, top_pos)
|
|
181
575
|
ent.instance_variable_set(:@open_pos, bottom_pos)
|
|
576
|
+
ent.instance_variable_set(:@targeted_platform, true)
|
|
577
|
+
ent.instance_variable_set(:@use, :plat_use)
|
|
182
578
|
else
|
|
183
579
|
# Normal: start at bottom, player steps on to ride up
|
|
184
580
|
ent.position = bottom_pos
|
|
185
581
|
ent.instance_variable_set(:@closed_pos, bottom_pos)
|
|
186
582
|
ent.instance_variable_set(:@open_pos, top_pos)
|
|
583
|
+
ent.instance_variable_set(:@use, :plat_trigger_use)
|
|
187
584
|
end
|
|
188
585
|
end
|
|
189
586
|
|
|
587
|
+
def apply_platform_noise(ent)
|
|
588
|
+
noise, noise1 = case ent.sounds
|
|
589
|
+
when 1
|
|
590
|
+
["plats/plat1.wav", "plats/plat2.wav"]
|
|
591
|
+
else
|
|
592
|
+
["plats/medplat1.wav", "plats/medplat2.wav"]
|
|
593
|
+
end
|
|
594
|
+
ent.instance_variable_set(:@noise, noise)
|
|
595
|
+
ent.instance_variable_set(:@noise1, noise1)
|
|
596
|
+
end
|
|
597
|
+
|
|
190
598
|
def setup_button(ent, model)
|
|
599
|
+
setup_bsp_push(ent)
|
|
600
|
+
ent.instance_variable_set(:@blocked, :button_blocked)
|
|
601
|
+
ent.instance_variable_set(:@use, :button_use)
|
|
191
602
|
dir = ent.forward_vector
|
|
603
|
+
ent.move_dir = dir
|
|
604
|
+
ent.angle = 0.0
|
|
605
|
+
ent.angles = Math::Vec3::ORIGIN
|
|
192
606
|
size = model.maxs - model.mins
|
|
193
607
|
move_dist = dir.x.abs * size.x + dir.y.abs * size.y + dir.z.abs * size.z
|
|
194
608
|
move_dist -= ent.lip
|
|
195
609
|
|
|
196
610
|
ent.instance_variable_set(:@open_pos, ent.position + dir * move_dist)
|
|
611
|
+
ent.instance_variable_set(:@max_health, ent.health) if ent.health.positive?
|
|
612
|
+
if ent.health.positive?
|
|
613
|
+
ent.instance_variable_set(:@takedamage, 1)
|
|
614
|
+
ent.instance_variable_set(:@th_die, :button_killed)
|
|
615
|
+
else
|
|
616
|
+
ent.instance_variable_set(:@touch, :button_touch)
|
|
617
|
+
end
|
|
618
|
+
ent.instance_variable_set(:@noise, button_noise(ent.sounds))
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def setup_train(ent, model)
|
|
622
|
+
raise "func_train without a target" if ent.target.nil? || ent.target.empty?
|
|
623
|
+
|
|
624
|
+
setup_bsp_push(ent)
|
|
625
|
+
ent.instance_variable_set(:@cnt, 1)
|
|
626
|
+
ent.instance_variable_set(:@blocked, :train_blocked)
|
|
627
|
+
ent.instance_variable_set(:@use, :train_use)
|
|
628
|
+
ent.properties["dmg"] = "2" if ent["dmg"].nil? || ent["dmg"].to_f.zero?
|
|
629
|
+
apply_train_noise(ent)
|
|
630
|
+
ent.instance_variable_set(:@train_mins, model.mins)
|
|
631
|
+
ent.classname = "train"
|
|
632
|
+
ent.think_time = 0.1
|
|
633
|
+
ent.instance_variable_set(:@think, :func_train_find)
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def setup_teleport_train(ent)
|
|
637
|
+
raise "func_train without a target" if ent.target.nil? || ent.target.empty?
|
|
638
|
+
|
|
639
|
+
ent.instance_variable_set(:@solid, 0)
|
|
640
|
+
ent.instance_variable_set(:@movetype, 7)
|
|
641
|
+
ent.instance_variable_set(:@cnt, 1)
|
|
642
|
+
ent.instance_variable_set(:@blocked, :train_blocked)
|
|
643
|
+
ent.instance_variable_set(:@use, :train_use)
|
|
644
|
+
ent.instance_variable_set(:@noise, "misc/null.wav")
|
|
645
|
+
ent.instance_variable_set(:@noise1, "misc/null.wav")
|
|
646
|
+
ent.instance_variable_set(:@avelocity, Math::Vec3.new(100.0, 200.0, 300.0))
|
|
647
|
+
ent.properties["model"] = "progs/teleport.mdl"
|
|
648
|
+
ent.instance_variable_set(:@train_mins, Math::Vec3::ORIGIN)
|
|
649
|
+
ent.think_time = 0.1
|
|
650
|
+
ent.instance_variable_set(:@think, :func_train_find)
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def link_doors
|
|
654
|
+
doors = @brush_entities.select { |ent| regular_door?(ent) }
|
|
655
|
+
doors.each do |door|
|
|
656
|
+
next if door.instance_variable_get(:@enemy)
|
|
657
|
+
|
|
658
|
+
group = collect_linked_door_chain(door, doors)
|
|
659
|
+
group.each_with_index do |member, index|
|
|
660
|
+
member.instance_variable_set(:@door_group, group)
|
|
661
|
+
member.instance_variable_set(:@owner, group.first)
|
|
662
|
+
member.instance_variable_set(:@enemy, group[(index + 1) % group.length])
|
|
663
|
+
end
|
|
664
|
+
apply_linked_door_owner_fields(group)
|
|
665
|
+
end
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def apply_linked_door_owner_fields(group)
|
|
669
|
+
owner = group.first
|
|
670
|
+
group.each do |member|
|
|
671
|
+
owner.health = member.health if member.health.positive?
|
|
672
|
+
owner.targetname = member.targetname if member.targetname
|
|
673
|
+
owner.message = member.message if member.message && !member.message.empty?
|
|
674
|
+
end
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
def collect_linked_door_chain(door, doors)
|
|
678
|
+
return [door] if (door.spawnflags & DOOR_DONT_LINK) != 0
|
|
679
|
+
|
|
680
|
+
group = [door]
|
|
681
|
+
current = door
|
|
682
|
+
loop do
|
|
683
|
+
candidate = doors[(doors.index(current) + 1)..]&.find do |later|
|
|
684
|
+
next false unless door_bounds_touch?(current, later)
|
|
685
|
+
raise "cross connected doors" if later.instance_variable_get(:@enemy)
|
|
686
|
+
|
|
687
|
+
true
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
unless candidate
|
|
691
|
+
current.instance_variable_set(:@enemy, door)
|
|
692
|
+
break
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
current.instance_variable_set(:@enemy, candidate)
|
|
696
|
+
group << candidate
|
|
697
|
+
current = candidate
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
group
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
def door_bounds_touch?(left, right)
|
|
704
|
+
left_model = @level.models[left.model_index]
|
|
705
|
+
right_model = @level.models[right.model_index]
|
|
706
|
+
return false unless left_model && right_model
|
|
707
|
+
|
|
708
|
+
left_mins = left.position + left_model.mins
|
|
709
|
+
left_maxs = left.position + left_model.maxs
|
|
710
|
+
right_mins = right.position + right_model.mins
|
|
711
|
+
right_maxs = right.position + right_model.maxs
|
|
712
|
+
|
|
713
|
+
left_mins.x <= right_maxs.x && left_maxs.x >= right_mins.x &&
|
|
714
|
+
left_mins.y <= right_maxs.y && left_maxs.y >= right_mins.y &&
|
|
715
|
+
left_mins.z <= right_maxs.z && left_maxs.z >= right_mins.z
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
def apply_train_noise(ent)
|
|
719
|
+
noise, noise1 = if ent.sounds == 1
|
|
720
|
+
["plats/train2.wav", "plats/train1.wav"]
|
|
721
|
+
else
|
|
722
|
+
["misc/null.wav", "misc/null.wav"]
|
|
723
|
+
end
|
|
724
|
+
ent.instance_variable_set(:@noise, noise)
|
|
725
|
+
ent.instance_variable_set(:@noise1, noise1)
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
def setup_bsp_push(ent)
|
|
729
|
+
ent.instance_variable_set(:@solid, 4)
|
|
730
|
+
ent.instance_variable_set(:@movetype, 7)
|
|
197
731
|
end
|
|
198
732
|
|
|
199
|
-
def
|
|
733
|
+
def button_noise(sounds)
|
|
734
|
+
case sounds
|
|
735
|
+
when 1 then "buttons/switch21.wav"
|
|
736
|
+
when 2 then "buttons/switch02.wav"
|
|
737
|
+
when 3 then "buttons/switch04.wav"
|
|
738
|
+
else "buttons/airbut1.wav"
|
|
739
|
+
end
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
def setup_episode_gate(ent)
|
|
743
|
+
if (@serverflags & ent.spawnflags).zero?
|
|
744
|
+
setup_inactive_gate(ent)
|
|
745
|
+
else
|
|
746
|
+
setup_static_brush_model(ent)
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
def setup_boss_gate(ent)
|
|
751
|
+
if (@serverflags & 15) == 15
|
|
752
|
+
setup_inactive_gate(ent)
|
|
753
|
+
else
|
|
754
|
+
setup_static_brush_model(ent)
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
def setup_inactive_gate(ent)
|
|
759
|
+
ent.model_index = nil
|
|
760
|
+
ent.instance_variable_set(:@movetype, 0)
|
|
761
|
+
ent.instance_variable_set(:@solid, 0)
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
def setup_static_brush_model(ent)
|
|
765
|
+
ent.angle = 0.0
|
|
766
|
+
ent.angles = Math::Vec3::ORIGIN
|
|
767
|
+
if ent.classname == "func_illusionary"
|
|
768
|
+
ent.instance_variable_set(:@movetype, 0)
|
|
769
|
+
ent.instance_variable_set(:@solid, 0)
|
|
770
|
+
ent.instance_variable_set(:@static, true)
|
|
771
|
+
else
|
|
772
|
+
ent.instance_variable_set(:@movetype, 7)
|
|
773
|
+
ent.instance_variable_set(:@solid, 4)
|
|
774
|
+
ent.instance_variable_set(:@use, :func_wall_use)
|
|
775
|
+
end
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
def update_door(ent, dt, player_pos, player_state)
|
|
779
|
+
if toggle_door?(ent) && ent.instance_variable_get(:@triggered) &&
|
|
780
|
+
[STATE_OPENING, STATE_OPEN].include?(ent.state)
|
|
781
|
+
ent.instance_variable_set(:@triggered, false)
|
|
782
|
+
ent.instance_variable_set(:@trigger_activator, nil)
|
|
783
|
+
ent.state = STATE_CLOSING
|
|
784
|
+
ent.instance_variable_set(:@move_fraction, 0.0)
|
|
785
|
+
elsif toggle_door?(ent) && ent.instance_variable_get(:@triggered) && ent.state == STATE_CLOSING
|
|
786
|
+
activator = ent.instance_variable_get(:@trigger_activator) || player_state || ent
|
|
787
|
+
ent.instance_variable_set(:@triggered, false)
|
|
788
|
+
ent.instance_variable_set(:@trigger_activator, nil)
|
|
789
|
+
ent.state = STATE_OPENING
|
|
790
|
+
use_door_targets(ent, activator: activator)
|
|
791
|
+
clear_linked_door_messages(ent)
|
|
792
|
+
ent.instance_variable_set(:@move_fraction, 0.0)
|
|
793
|
+
end
|
|
794
|
+
|
|
200
795
|
case ent.state
|
|
201
796
|
when STATE_IDLE, STATE_CLOSED
|
|
202
|
-
# Check if player is
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
797
|
+
# Check if player is touching the door, its generated trigger field, or if targeted.
|
|
798
|
+
door_touch = door_model_touch?(ent, player_pos)
|
|
799
|
+
trigger_touch = door_trigger_field_touch?(ent, player_pos)
|
|
800
|
+
keyed_door = linked_door_keyed?(ent)
|
|
801
|
+
key_touch = keyed_door && door_touch
|
|
802
|
+
triggered = ent.instance_variable_get(:@triggered)
|
|
803
|
+
door_touch_allowed = !door_touch || door_touch_allowed?(ent, player_state)
|
|
804
|
+
trigger_touch_allowed = !trigger_touch || door_trigger_touch_allowed?(ent)
|
|
805
|
+
touch_door_message(ent, player_state) if door_touch && door_touch_allowed
|
|
806
|
+
touch_opens = if key_touch
|
|
807
|
+
door_touch_allowed && unlock_door?(ent, player_state)
|
|
808
|
+
elsif trigger_touch
|
|
809
|
+
trigger_touch_allowed && unlock_door?(ent, player_state)
|
|
810
|
+
else
|
|
811
|
+
false
|
|
812
|
+
end
|
|
813
|
+
if triggered || (touch_activated_door?(ent) && touch_opens)
|
|
814
|
+
activator = ent.instance_variable_get(:@trigger_activator) || player_state || ent
|
|
815
|
+
use_door(ent, activator: activator)
|
|
206
816
|
end
|
|
207
817
|
when STATE_OPENING
|
|
818
|
+
if ent.instance_variable_get(:@activated_at_time) == @time
|
|
819
|
+
ent.instance_variable_set(:@activated_at_time, nil)
|
|
820
|
+
return
|
|
821
|
+
end
|
|
822
|
+
|
|
208
823
|
move_toward(ent, ent.instance_variable_get(:@open_pos), dt)
|
|
824
|
+
door_blocked(ent, player_pos, player_state)
|
|
209
825
|
if ent.instance_variable_get(:@move_fraction) >= 1.0
|
|
210
826
|
ent.state = STATE_OPEN
|
|
211
827
|
ent.think_time = ent.wait
|
|
828
|
+
play_brush_noise(ent, :@noise1)
|
|
212
829
|
end
|
|
213
830
|
when STATE_OPEN
|
|
831
|
+
if ent.instance_variable_get(:@triggered) && !toggle_door?(ent)
|
|
832
|
+
ent.instance_variable_set(:@triggered, false)
|
|
833
|
+
ent.instance_variable_set(:@trigger_activator, nil)
|
|
834
|
+
ent.think_time = ent.wait
|
|
835
|
+
end
|
|
214
836
|
ent.think_time -= dt
|
|
215
|
-
if ent.think_time <= 0 && ent.wait >= 0
|
|
837
|
+
if ent.think_time <= 0 && ent.wait >= 0 && !toggle_door?(ent)
|
|
838
|
+
ent.health = ent.instance_variable_get(:@max_health) if shootable_door?(ent)
|
|
839
|
+
ent.instance_variable_set(:@shot_disabled, false)
|
|
840
|
+
ent.instance_variable_set(:@takedamage, 1) if shootable_door?(ent)
|
|
841
|
+
play_brush_noise(ent, :@noise2)
|
|
216
842
|
ent.state = STATE_CLOSING
|
|
217
843
|
end
|
|
218
844
|
when STATE_CLOSING
|
|
845
|
+
if ent.instance_variable_get(:@activated_at_time) == @time
|
|
846
|
+
ent.instance_variable_set(:@activated_at_time, nil)
|
|
847
|
+
return
|
|
848
|
+
end
|
|
849
|
+
|
|
219
850
|
move_toward(ent, ent.instance_variable_get(:@closed_pos), dt)
|
|
851
|
+
door_blocked(ent, player_pos, player_state)
|
|
220
852
|
if ent.instance_variable_get(:@move_fraction) >= 1.0
|
|
221
853
|
ent.state = STATE_CLOSED
|
|
854
|
+
play_brush_noise(ent, :@noise1)
|
|
222
855
|
end
|
|
223
856
|
end
|
|
224
857
|
end
|
|
225
858
|
|
|
226
|
-
def update_platform(ent, dt, player_pos)
|
|
859
|
+
def update_platform(ent, dt, player_pos, player_state = nil)
|
|
227
860
|
case ent.state
|
|
228
|
-
when STATE_IDLE
|
|
861
|
+
when STATE_IDLE, STATE_CLOSED
|
|
229
862
|
model = @level.models[ent.model_index]
|
|
230
863
|
triggered = ent.instance_variable_get(:@triggered)
|
|
231
|
-
|
|
864
|
+
center_touch = model && platform_center_touch_actor?(player_state) &&
|
|
865
|
+
platform_center_trigger_touch?(ent, model, player_pos)
|
|
866
|
+
targeted_waiting_for_first_use = ent.targetname && ent.instance_variable_get(:@targeted_platform)
|
|
232
867
|
|
|
233
868
|
# Targeted platforms only react to button triggers.
|
|
234
|
-
#
|
|
235
|
-
should_move = if
|
|
869
|
+
# Once lowered, they behave like normal platforms again.
|
|
870
|
+
should_move = if targeted_waiting_for_first_use
|
|
871
|
+
if triggered && ent.instance_variable_get(:@platform_used)
|
|
872
|
+
raise "plat_use: not in up state"
|
|
873
|
+
end
|
|
236
874
|
triggered
|
|
237
875
|
else
|
|
238
|
-
triggered ||
|
|
876
|
+
triggered || center_touch
|
|
239
877
|
end
|
|
240
878
|
|
|
241
879
|
if should_move
|
|
880
|
+
ent.instance_variable_set(:@platform_used, true) if ent.instance_variable_get(:@targeted_platform)
|
|
881
|
+
play_brush_noise(ent, :@noise)
|
|
242
882
|
ent.state = STATE_OPENING
|
|
243
883
|
ent.instance_variable_set(:@triggered, false)
|
|
244
884
|
end
|
|
245
885
|
when STATE_OPENING
|
|
886
|
+
if ent.instance_variable_get(:@activated_at_time) == @time
|
|
887
|
+
ent.instance_variable_set(:@activated_at_time, nil)
|
|
888
|
+
return
|
|
889
|
+
end
|
|
890
|
+
|
|
246
891
|
move_toward(ent, ent.instance_variable_get(:@open_pos), dt)
|
|
892
|
+
platform_blocked(ent, player_pos, player_state)
|
|
247
893
|
if ent.instance_variable_get(:@move_fraction) >= 1.0
|
|
248
|
-
ent.
|
|
249
|
-
|
|
894
|
+
if ent.instance_variable_get(:@targeted_platform)
|
|
895
|
+
enable_normal_platform_operation(ent)
|
|
896
|
+
ent.state = STATE_CLOSED
|
|
897
|
+
ent.think_time = 0.0
|
|
898
|
+
else
|
|
899
|
+
ent.state = STATE_OPEN
|
|
900
|
+
ent.think_time = ent.wait
|
|
901
|
+
end
|
|
902
|
+
play_brush_noise(ent, :@noise1)
|
|
250
903
|
end
|
|
251
904
|
when STATE_OPEN
|
|
252
|
-
ent.
|
|
905
|
+
model = @level.models[ent.model_index]
|
|
906
|
+
if model && on_top_of?(ent, model, player_pos)
|
|
907
|
+
ent.think_time = 1.0
|
|
908
|
+
else
|
|
909
|
+
ent.think_time -= dt
|
|
910
|
+
end
|
|
253
911
|
if ent.think_time <= 0
|
|
912
|
+
play_brush_noise(ent, :@noise)
|
|
254
913
|
ent.state = STATE_CLOSING
|
|
255
914
|
end
|
|
256
915
|
when STATE_CLOSING
|
|
916
|
+
if ent.instance_variable_get(:@activated_at_time) == @time
|
|
917
|
+
ent.instance_variable_set(:@activated_at_time, nil)
|
|
918
|
+
return
|
|
919
|
+
end
|
|
920
|
+
|
|
257
921
|
move_toward(ent, ent.instance_variable_get(:@closed_pos), dt)
|
|
922
|
+
platform_blocked(ent, player_pos, player_state)
|
|
258
923
|
if ent.instance_variable_get(:@move_fraction) >= 1.0
|
|
259
|
-
ent.state =
|
|
924
|
+
ent.state = STATE_CLOSED
|
|
925
|
+
play_brush_noise(ent, :@noise1)
|
|
260
926
|
end
|
|
261
927
|
end
|
|
262
928
|
end
|
|
263
929
|
|
|
264
|
-
def
|
|
930
|
+
def enable_normal_platform_operation(ent)
|
|
931
|
+
top_pos = ent.instance_variable_get(:@closed_pos)
|
|
932
|
+
bottom_pos = ent.instance_variable_get(:@open_pos)
|
|
933
|
+
ent.instance_variable_set(:@closed_pos, bottom_pos)
|
|
934
|
+
ent.instance_variable_set(:@open_pos, top_pos)
|
|
935
|
+
ent.instance_variable_set(:@targeted_platform, false)
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
def update_button(ent, dt, player_pos, player_state = nil)
|
|
265
939
|
case ent.state
|
|
266
940
|
when STATE_IDLE, STATE_CLOSED
|
|
267
|
-
|
|
268
|
-
|
|
941
|
+
# button_touch fires on brush contact. The expansion is wider than
|
|
942
|
+
# the 16-unit player half-width because this port's wall collision
|
|
943
|
+
# can hold the player a bit off flush surfaces; still much tighter
|
|
944
|
+
# than a center-distance sphere.
|
|
945
|
+
touched = !shootable_button?(ent) && door_model_touch?(ent, player_pos, player_radius: BUTTON_TOUCH_RADIUS)
|
|
946
|
+
if touched || ent.instance_variable_get(:@triggered)
|
|
947
|
+
ent.instance_variable_set(:@button_activator, player_state) if touched && player_state
|
|
948
|
+
fire_button(ent)
|
|
269
949
|
ent.instance_variable_set(:@triggered, false)
|
|
270
|
-
fire_targets(ent.target)
|
|
271
950
|
end
|
|
272
951
|
when STATE_OPENING
|
|
273
952
|
move_toward(ent, ent.instance_variable_get(:@open_pos), dt)
|
|
274
953
|
if ent.instance_variable_get(:@move_fraction) >= 1.0
|
|
275
954
|
ent.state = STATE_OPEN
|
|
276
955
|
ent.think_time = ent.wait
|
|
956
|
+
activator = ent.instance_variable_get(:@button_activator) ||
|
|
957
|
+
ent.instance_variable_get(:@trigger_activator) ||
|
|
958
|
+
ent
|
|
959
|
+
use_targets(ent, activator: activator)
|
|
960
|
+
ent.instance_variable_set(:@button_activator, nil)
|
|
961
|
+
ent.instance_variable_set(:@trigger_activator, nil)
|
|
962
|
+
ent.frame = 1
|
|
277
963
|
end
|
|
278
964
|
when STATE_OPEN
|
|
279
965
|
ent.think_time -= dt
|
|
966
|
+
# QC button_wait schedules the return even for wait -1 (the time is
|
|
967
|
+
# already in the past), so such buttons pop back out immediately
|
|
280
968
|
if ent.think_time <= 0
|
|
281
969
|
ent.state = STATE_CLOSING
|
|
970
|
+
ent.frame = 0
|
|
971
|
+
if shootable_button?(ent)
|
|
972
|
+
ent.instance_variable_set(:@shot_disabled, false)
|
|
973
|
+
ent.instance_variable_set(:@takedamage, 1)
|
|
974
|
+
end
|
|
282
975
|
end
|
|
283
976
|
when STATE_CLOSING
|
|
977
|
+
if !shootable_button?(ent) && door_model_touch?(ent, player_pos, player_radius: BUTTON_TOUCH_RADIUS)
|
|
978
|
+
ent.instance_variable_set(:@button_activator, player_state) if player_state
|
|
979
|
+
fire_button(ent)
|
|
980
|
+
ent.instance_variable_set(:@move_fraction, 0.0)
|
|
981
|
+
return
|
|
982
|
+
end
|
|
284
983
|
move_toward(ent, ent.instance_variable_get(:@closed_pos), dt)
|
|
285
984
|
if ent.instance_variable_get(:@move_fraction) >= 1.0
|
|
286
985
|
ent.state = STATE_CLOSED
|
|
@@ -288,40 +987,143 @@ module Quake
|
|
|
288
987
|
end
|
|
289
988
|
end
|
|
290
989
|
|
|
291
|
-
def update_train(ent, dt)
|
|
292
|
-
|
|
990
|
+
def update_train(ent, dt, player_pos, player_state = nil)
|
|
991
|
+
cooldown = ent.instance_variable_get(:@blocked_cooldown).to_f
|
|
992
|
+
ent.instance_variable_set(:@blocked_cooldown, [cooldown - dt, 0.0].max) if cooldown.positive?
|
|
993
|
+
|
|
994
|
+
return if update_train_start_think(ent, dt)
|
|
995
|
+
|
|
996
|
+
if ent.state == STATE_IDLE && ent.instance_variable_get(:@triggered)
|
|
997
|
+
ent.instance_variable_set(:@triggered, false)
|
|
998
|
+
start_train_next(ent)
|
|
999
|
+
end
|
|
1000
|
+
|
|
1001
|
+
if ent.state == STATE_OPEN
|
|
1002
|
+
ent.think_time -= dt
|
|
1003
|
+
start_train_next(ent) if ent.think_time <= 0
|
|
1004
|
+
return
|
|
1005
|
+
end
|
|
1006
|
+
|
|
293
1007
|
return unless ent.state == STATE_OPENING
|
|
294
1008
|
|
|
295
1009
|
move_toward(ent, ent.instance_variable_get(:@open_pos), dt)
|
|
1010
|
+
train_blocked(ent, player_pos, player_state)
|
|
296
1011
|
if ent.instance_variable_get(:@move_fraction) >= 1.0
|
|
297
|
-
ent.state =
|
|
1012
|
+
ent.state = STATE_OPEN
|
|
1013
|
+
ent.think_time = ent.wait.nonzero? ? ent.wait : 0.1
|
|
1014
|
+
play_brush_noise(ent, :@noise) if ent.wait.nonzero?
|
|
1015
|
+
end
|
|
1016
|
+
end
|
|
1017
|
+
|
|
1018
|
+
def start_train_next(ent)
|
|
1019
|
+
raise "train_next: no next target" if ent.target.nil? || ent.target.empty?
|
|
1020
|
+
|
|
1021
|
+
target = @target_map[ent.target]&.first
|
|
1022
|
+
raise "train_next: no next target" unless target
|
|
1023
|
+
|
|
1024
|
+
ent.target = target.target
|
|
1025
|
+
raise "train_next: no next target" if ent.target.nil? || ent.target.empty?
|
|
1026
|
+
|
|
1027
|
+
ent.wait = target.wait.nonzero? ? target.wait : 0.0
|
|
1028
|
+
mins = ent.instance_variable_get(:@train_mins) || Math::Vec3::ORIGIN
|
|
1029
|
+
ent.instance_variable_set(:@open_pos, target.position - mins)
|
|
1030
|
+
ent.instance_variable_set(:@move_fraction, 0.0)
|
|
1031
|
+
ent.instance_variable_set(:@think, nil)
|
|
1032
|
+
ent.think_time = 0.0
|
|
1033
|
+
play_brush_noise(ent, :@noise1)
|
|
1034
|
+
ent.state = STATE_OPENING
|
|
1035
|
+
end
|
|
1036
|
+
|
|
1037
|
+
def update_train_start_think(ent, dt)
|
|
1038
|
+
think = ent.instance_variable_get(:@think)
|
|
1039
|
+
return false unless %i[func_train_find train_next].include?(think)
|
|
1040
|
+
return false if think == :func_train_find && ent.instance_variable_get(:@train_found)
|
|
1041
|
+
|
|
1042
|
+
ent.think_time -= dt
|
|
1043
|
+
return true if ent.think_time.positive?
|
|
1044
|
+
|
|
1045
|
+
if think == :func_train_find
|
|
1046
|
+
train_find(ent)
|
|
1047
|
+
else
|
|
1048
|
+
start_train_next(ent)
|
|
1049
|
+
end
|
|
1050
|
+
true
|
|
1051
|
+
end
|
|
1052
|
+
|
|
1053
|
+
def train_find(ent)
|
|
1054
|
+
target = @target_map[ent.target]&.first
|
|
1055
|
+
if target
|
|
1056
|
+
mins = ent.instance_variable_get(:@train_mins) || Math::Vec3::ORIGIN
|
|
1057
|
+
ent.position = target.position - mins
|
|
1058
|
+
ent.target = target.target
|
|
1059
|
+
else
|
|
1060
|
+
ent.target = nil
|
|
298
1061
|
end
|
|
1062
|
+
ent.instance_variable_set(:@train_found, true)
|
|
1063
|
+
|
|
1064
|
+
return if ent.targetname
|
|
1065
|
+
|
|
1066
|
+
ent.think_time = 0.1
|
|
1067
|
+
ent.instance_variable_set(:@think, :train_next)
|
|
299
1068
|
end
|
|
300
1069
|
|
|
301
1070
|
def move_toward(ent, target, dt)
|
|
302
1071
|
current = ent.position
|
|
303
1072
|
diff = target - current
|
|
304
1073
|
dist = diff.length
|
|
1074
|
+
move_end = ent.instance_variable_get(:@move_end_pos)
|
|
305
1075
|
|
|
306
|
-
# Already at target
|
|
307
1076
|
if dist < 0.1
|
|
1077
|
+
unless move_end == target
|
|
1078
|
+
ent.instance_variable_set(:@move_start_pos, current)
|
|
1079
|
+
ent.instance_variable_set(:@move_end_pos, target)
|
|
1080
|
+
ent.instance_variable_set(:@move_elapsed, 0.0)
|
|
1081
|
+
ent.instance_variable_set(:@move_duration, 0.1)
|
|
1082
|
+
end
|
|
1083
|
+
elapsed = ent.instance_variable_get(:@move_elapsed).to_f + dt
|
|
1084
|
+
duration = ent.instance_variable_get(:@move_duration).to_f
|
|
1085
|
+
ent.instance_variable_set(:@move_elapsed, elapsed)
|
|
308
1086
|
ent.position = target
|
|
309
|
-
|
|
1087
|
+
if elapsed >= duration
|
|
1088
|
+
ent.instance_variable_set(:@move_fraction, 1.0)
|
|
1089
|
+
clear_move_timing(ent)
|
|
1090
|
+
else
|
|
1091
|
+
ent.instance_variable_set(:@move_fraction, 0.0)
|
|
1092
|
+
end
|
|
310
1093
|
return
|
|
311
1094
|
end
|
|
312
1095
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
1096
|
+
unless move_end == target
|
|
1097
|
+
ent.instance_variable_set(:@move_start_pos, current)
|
|
1098
|
+
ent.instance_variable_set(:@move_end_pos, target)
|
|
1099
|
+
ent.instance_variable_set(:@move_elapsed, 0.0)
|
|
1100
|
+
ent.instance_variable_set(:@move_duration, [dist / ent.speed, 0.03].max)
|
|
1101
|
+
end
|
|
1102
|
+
|
|
1103
|
+
elapsed = ent.instance_variable_get(:@move_elapsed).to_f + dt
|
|
1104
|
+
duration = ent.instance_variable_get(:@move_duration).to_f
|
|
1105
|
+
fraction = duration.positive? ? elapsed / duration : 1.0
|
|
1106
|
+
ent.instance_variable_set(:@move_elapsed, elapsed)
|
|
1107
|
+
|
|
1108
|
+
if fraction >= 1.0
|
|
316
1109
|
ent.position = target
|
|
317
1110
|
ent.instance_variable_set(:@move_fraction, 1.0)
|
|
1111
|
+
clear_move_timing(ent)
|
|
318
1112
|
else
|
|
319
|
-
|
|
320
|
-
|
|
1113
|
+
start = ent.instance_variable_get(:@move_start_pos)
|
|
1114
|
+
total_diff = target - start
|
|
1115
|
+
ent.position = start + total_diff * fraction
|
|
321
1116
|
ent.instance_variable_set(:@move_fraction, 0.0)
|
|
322
1117
|
end
|
|
323
1118
|
end
|
|
324
1119
|
|
|
1120
|
+
def clear_move_timing(ent)
|
|
1121
|
+
ent.instance_variable_set(:@move_start_pos, nil)
|
|
1122
|
+
ent.instance_variable_set(:@move_end_pos, nil)
|
|
1123
|
+
ent.instance_variable_set(:@move_elapsed, 0.0)
|
|
1124
|
+
ent.instance_variable_set(:@move_duration, 0.0)
|
|
1125
|
+
end
|
|
1126
|
+
|
|
325
1127
|
def near_entity?(ent, player_pos, radius)
|
|
326
1128
|
model = @level.models[ent.model_index]
|
|
327
1129
|
return false unless model
|
|
@@ -334,26 +1136,973 @@ module Quake
|
|
|
334
1136
|
(dx * dx + dy * dy + dz * dz) < radius * radius
|
|
335
1137
|
end
|
|
336
1138
|
|
|
1139
|
+
def door_trigger_field_touch?(ent, player_pos)
|
|
1140
|
+
return false unless touch_activated_door?(ent)
|
|
1141
|
+
return false if linked_door_keyed?(ent)
|
|
1142
|
+
|
|
1143
|
+
mins, maxs = linked_door_group_bounds(ent)
|
|
1144
|
+
return false unless mins && maxs
|
|
1145
|
+
|
|
1146
|
+
player_pos.x >= mins.x - 60.0 &&
|
|
1147
|
+
player_pos.x <= maxs.x + 60.0 &&
|
|
1148
|
+
player_pos.y >= mins.y - 60.0 &&
|
|
1149
|
+
player_pos.y <= maxs.y + 60.0 &&
|
|
1150
|
+
player_pos.z >= mins.z - 8.0 &&
|
|
1151
|
+
player_pos.z <= maxs.z + 8.0
|
|
1152
|
+
end
|
|
1153
|
+
|
|
1154
|
+
def linked_door_group_bounds(ent)
|
|
1155
|
+
mins = nil
|
|
1156
|
+
maxs = nil
|
|
1157
|
+
linked_door_group(ent).each do |member|
|
|
1158
|
+
model = @level.models[member.model_index]
|
|
1159
|
+
next unless model
|
|
1160
|
+
|
|
1161
|
+
member_mins = member.position + model.mins
|
|
1162
|
+
member_maxs = member.position + model.maxs
|
|
1163
|
+
mins = member_mins if mins.nil?
|
|
1164
|
+
maxs = member_maxs if maxs.nil?
|
|
1165
|
+
mins = Math::Vec3.new([mins.x, member_mins.x].min, [mins.y, member_mins.y].min, [mins.z, member_mins.z].min)
|
|
1166
|
+
maxs = Math::Vec3.new([maxs.x, member_maxs.x].max, [maxs.y, member_maxs.y].max, [maxs.z, member_maxs.z].max)
|
|
1167
|
+
end
|
|
1168
|
+
[mins, maxs]
|
|
1169
|
+
end
|
|
1170
|
+
|
|
337
1171
|
def on_top_of?(ent, model, player_pos)
|
|
338
1172
|
# Check if player is standing on top of the platform.
|
|
339
1173
|
# Tolerance: -8 to +24 units above the surface (player feet within
|
|
340
1174
|
# one stair step of the platform top).
|
|
341
1175
|
top_z = ent.position.z + model.maxs.z
|
|
342
|
-
player_pos.z >= top_z
|
|
1176
|
+
player_pos.z >= top_z + 16 && player_pos.z <= top_z + 48 &&
|
|
343
1177
|
player_pos.x >= ent.position.x + model.mins.x &&
|
|
344
1178
|
player_pos.x <= ent.position.x + model.maxs.x &&
|
|
345
1179
|
player_pos.y >= ent.position.y + model.mins.y &&
|
|
346
1180
|
player_pos.y <= ent.position.y + model.maxs.y
|
|
347
1181
|
end
|
|
348
1182
|
|
|
349
|
-
def
|
|
350
|
-
|
|
1183
|
+
def door_model_touch?(ent, player_pos, player_radius: 16.0)
|
|
1184
|
+
model = @level.models[ent.model_index]
|
|
1185
|
+
return false unless model
|
|
351
1186
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
ent.
|
|
355
|
-
|
|
356
|
-
ent.
|
|
1187
|
+
player_pos.x >= ent.position.x + model.mins.x - player_radius &&
|
|
1188
|
+
player_pos.x <= ent.position.x + model.maxs.x + player_radius &&
|
|
1189
|
+
player_pos.y >= ent.position.y + model.mins.y - player_radius &&
|
|
1190
|
+
player_pos.y <= ent.position.y + model.maxs.y + player_radius &&
|
|
1191
|
+
player_pos.z >= ent.position.z + model.mins.z - player_radius &&
|
|
1192
|
+
player_pos.z <= ent.position.z + model.maxs.z + player_radius
|
|
1193
|
+
end
|
|
1194
|
+
|
|
1195
|
+
def platform_center_trigger_touch?(ent, model, player_pos)
|
|
1196
|
+
lower_origin_z, upper_origin_z = [
|
|
1197
|
+
ent.instance_variable_get(:@closed_pos).z,
|
|
1198
|
+
ent.instance_variable_get(:@open_pos).z
|
|
1199
|
+
].minmax
|
|
1200
|
+
bottom_top_z = lower_origin_z + model.maxs.z
|
|
1201
|
+
top_trigger_z = if (ent.spawnflags & PLAT_LOW_TRIGGER) != 0
|
|
1202
|
+
bottom_top_z + 8.0
|
|
1203
|
+
else
|
|
1204
|
+
upper_origin_z + model.maxs.z + 8.0
|
|
1205
|
+
end
|
|
1206
|
+
|
|
1207
|
+
return false unless player_pos.z >= bottom_top_z
|
|
1208
|
+
return false unless player_pos.z <= top_trigger_z
|
|
1209
|
+
|
|
1210
|
+
mins = ent.position + model.mins
|
|
1211
|
+
maxs = ent.position + model.maxs
|
|
1212
|
+
size = model.maxs - model.mins
|
|
1213
|
+
min_x = mins.x + 25.0
|
|
1214
|
+
max_x = maxs.x - 25.0
|
|
1215
|
+
min_y = mins.y + 25.0
|
|
1216
|
+
max_y = maxs.y - 25.0
|
|
1217
|
+
|
|
1218
|
+
if size.x <= 50.0
|
|
1219
|
+
min_x = (mins.x + maxs.x) / 2.0
|
|
1220
|
+
max_x = min_x + 1.0
|
|
1221
|
+
end
|
|
1222
|
+
if size.y <= 50.0
|
|
1223
|
+
min_y = (mins.y + maxs.y) / 2.0
|
|
1224
|
+
max_y = min_y + 1.0
|
|
1225
|
+
end
|
|
1226
|
+
|
|
1227
|
+
player_pos.x >= min_x && player_pos.x <= max_x &&
|
|
1228
|
+
player_pos.y >= min_y && player_pos.y <= max_y
|
|
1229
|
+
end
|
|
1230
|
+
|
|
1231
|
+
def platform_center_touch_actor?(player_state)
|
|
1232
|
+
return true unless player_state
|
|
1233
|
+
|
|
1234
|
+
player_state.classname == "player" && player_state.alive?
|
|
1235
|
+
end
|
|
1236
|
+
|
|
1237
|
+
def door_blocked(ent, player_pos, player_state)
|
|
1238
|
+
return unless player_state&.alive?
|
|
1239
|
+
# A player standing on top rides the pusher (SV_PushMove), same as
|
|
1240
|
+
# for plats -- vertical doors are Quake's elevators.
|
|
1241
|
+
return if riding_on_top?(ent, player_pos)
|
|
1242
|
+
return unless player_blocking_brush?(ent, player_pos)
|
|
1243
|
+
|
|
1244
|
+
damage = (ent["dmg"] || "2").to_f
|
|
1245
|
+
player_state.deathtype = "squish" if player_state.respond_to?(:deathtype=)
|
|
1246
|
+
player_state.take_damage(damage)
|
|
1247
|
+
return if ent.wait.negative?
|
|
1248
|
+
|
|
1249
|
+
ent.state = ent.state == STATE_CLOSING ? STATE_OPENING : STATE_CLOSING
|
|
1250
|
+
play_brush_noise(ent, :@noise2)
|
|
1251
|
+
ent.instance_variable_set(:@move_fraction, 0.0)
|
|
1252
|
+
end
|
|
1253
|
+
|
|
1254
|
+
def platform_blocked(ent, player_pos, player_state)
|
|
1255
|
+
return unless player_state&.alive?
|
|
1256
|
+
# A player standing on top is a rider carried with the pusher
|
|
1257
|
+
# (SV_PushMove), not a blocker. player_pos is pre-carry here, so
|
|
1258
|
+
# judge "on top" against the platform's pre-move position.
|
|
1259
|
+
return if riding_on_top?(ent, player_pos)
|
|
1260
|
+
return unless player_blocking_brush?(ent, player_pos)
|
|
1261
|
+
|
|
1262
|
+
player_state.deathtype = "squish" if player_state.respond_to?(:deathtype=)
|
|
1263
|
+
player_state.take_damage(1)
|
|
1264
|
+
ent.state = ent.state == STATE_CLOSING ? STATE_OPENING : STATE_CLOSING
|
|
1265
|
+
play_brush_noise(ent, :@noise)
|
|
1266
|
+
ent.instance_variable_set(:@move_fraction, 0.0)
|
|
1267
|
+
end
|
|
1268
|
+
|
|
1269
|
+
def riding_on_top?(ent, player_pos)
|
|
1270
|
+
model = @level.models[ent.model_index]
|
|
1271
|
+
return false unless model
|
|
1272
|
+
|
|
1273
|
+
prev = ent.instance_variable_get(:@prev_pos) || ent.position
|
|
1274
|
+
old_top_z = prev.z + model.maxs.z
|
|
1275
|
+
# Rider origins sit ~24 above the surface; a player squeezed from
|
|
1276
|
+
# the side has an origin below the top
|
|
1277
|
+
player_pos.z >= old_top_z + 16 &&
|
|
1278
|
+
player_pos.x >= ent.position.x + model.mins.x &&
|
|
1279
|
+
player_pos.x <= ent.position.x + model.maxs.x &&
|
|
1280
|
+
player_pos.y >= ent.position.y + model.mins.y &&
|
|
1281
|
+
player_pos.y <= ent.position.y + model.maxs.y
|
|
1282
|
+
end
|
|
1283
|
+
|
|
1284
|
+
def train_blocked(ent, player_pos, player_state)
|
|
1285
|
+
return unless player_state&.alive?
|
|
1286
|
+
return unless player_blocking_brush?(ent, player_pos)
|
|
1287
|
+
return if ent.instance_variable_get(:@blocked_cooldown).to_f.positive?
|
|
1288
|
+
|
|
1289
|
+
damage = (ent["dmg"] || "2").to_f
|
|
1290
|
+
player_state.deathtype = "squish" if player_state.respond_to?(:deathtype=)
|
|
1291
|
+
player_state.take_damage(damage)
|
|
1292
|
+
ent.instance_variable_set(:@blocked_cooldown, 0.5)
|
|
1293
|
+
end
|
|
1294
|
+
|
|
1295
|
+
def player_blocking_brush?(ent, player_pos)
|
|
1296
|
+
model = @level.models[ent.model_index]
|
|
1297
|
+
return false unless model
|
|
1298
|
+
|
|
1299
|
+
player_pos.x >= ent.position.x + model.mins.x &&
|
|
1300
|
+
player_pos.x <= ent.position.x + model.maxs.x &&
|
|
1301
|
+
player_pos.y >= ent.position.y + model.mins.y &&
|
|
1302
|
+
player_pos.y <= ent.position.y + model.maxs.y &&
|
|
1303
|
+
player_pos.z >= ent.position.z + model.mins.z &&
|
|
1304
|
+
player_pos.z <= ent.position.z + model.maxs.z
|
|
1305
|
+
end
|
|
1306
|
+
|
|
1307
|
+
def unlock_door?(ent, player_state)
|
|
1308
|
+
required_key = ent.instance_variable_get(:@required_key)
|
|
1309
|
+
return true unless required_key
|
|
1310
|
+
unless player_state&.has_key?(required_key)
|
|
1311
|
+
centerprint_key_required(player_state, required_key)
|
|
1312
|
+
play_brush_noise(ent, :@noise3)
|
|
1313
|
+
return false
|
|
1314
|
+
end
|
|
1315
|
+
|
|
1316
|
+
player_state.consume_key(required_key)
|
|
1317
|
+
disable_key_door_touch(ent)
|
|
1318
|
+
true
|
|
1319
|
+
end
|
|
1320
|
+
|
|
1321
|
+
def disable_key_door_touch(ent)
|
|
1322
|
+
ent.instance_variable_set(:@touch, :sub_null)
|
|
1323
|
+
enemy = ent.instance_variable_get(:@enemy)
|
|
1324
|
+
enemy.instance_variable_set(:@touch, :sub_null) if enemy
|
|
1325
|
+
end
|
|
1326
|
+
|
|
1327
|
+
def door_touch_allowed?(ent, player_state)
|
|
1328
|
+
return true unless player_state&.classname == "player"
|
|
1329
|
+
owner = linked_door_owner(ent)
|
|
1330
|
+
return false if owner.instance_variable_get(:@attack_finished).to_f > @time
|
|
1331
|
+
|
|
1332
|
+
owner.instance_variable_set(:@attack_finished, @time + 2.0)
|
|
1333
|
+
true
|
|
1334
|
+
end
|
|
1335
|
+
|
|
1336
|
+
def door_trigger_touch_allowed?(ent)
|
|
1337
|
+
owner = linked_door_owner(ent)
|
|
1338
|
+
return false if owner.instance_variable_get(:@trigger_attack_finished).to_f > @time
|
|
1339
|
+
|
|
1340
|
+
owner.instance_variable_set(:@trigger_attack_finished, @time + 1.0)
|
|
1341
|
+
true
|
|
1342
|
+
end
|
|
1343
|
+
|
|
1344
|
+
def centerprint_key_required(player_state, required_key)
|
|
1345
|
+
return unless player_state
|
|
1346
|
+
|
|
1347
|
+
player_state.instance_variable_set(:@centerprint, key_required_message(required_key))
|
|
1348
|
+
end
|
|
1349
|
+
|
|
1350
|
+
def touch_door_message(ent, player_state)
|
|
1351
|
+
return unless player_state&.classname == "player"
|
|
1352
|
+
message = linked_door_owner(ent).message
|
|
1353
|
+
return if message.nil? || message.empty?
|
|
1354
|
+
player_state.instance_variable_set(:@centerprint, message)
|
|
1355
|
+
play_default_message_sound
|
|
1356
|
+
end
|
|
1357
|
+
|
|
1358
|
+
def key_required_message(required_key)
|
|
1359
|
+
key_name = case [required_key, @worldtype]
|
|
1360
|
+
when [:silver, 1] then "silver runekey"
|
|
1361
|
+
when [:silver, 2] then "silver keycard"
|
|
1362
|
+
when [:gold, 1] then "gold runekey"
|
|
1363
|
+
when [:gold, 2] then "gold keycard"
|
|
1364
|
+
when [:gold, 0] then "gold key"
|
|
1365
|
+
else "silver key"
|
|
1366
|
+
end
|
|
1367
|
+
"You need the #{key_name}"
|
|
1368
|
+
end
|
|
1369
|
+
|
|
1370
|
+
def update_trigger_cooldown(ent, dt)
|
|
1371
|
+
return unless ["trigger_multiple", "trigger_hurt"].include?(ent.classname)
|
|
1372
|
+
return unless ent.think_time.positive?
|
|
1373
|
+
|
|
1374
|
+
previous = ent.think_time
|
|
1375
|
+
ent.think_time = [ent.think_time - dt, 0.0].max
|
|
1376
|
+
ent.think_time = 0.0 if ent.think_time <= 0.000001
|
|
1377
|
+
return unless previous.positive? && ent.think_time.zero?
|
|
1378
|
+
|
|
1379
|
+
if ent.classname == "trigger_hurt"
|
|
1380
|
+
ent.instance_variable_set(:@touch_disabled, false)
|
|
1381
|
+
ent.instance_variable_set(:@solid, 1)
|
|
1382
|
+
ent.think_time = -1.0
|
|
1383
|
+
end
|
|
1384
|
+
restore_health_trigger(ent)
|
|
1385
|
+
end
|
|
1386
|
+
|
|
1387
|
+
def update_scheduled_remove(ent, dt)
|
|
1388
|
+
return unless ent.instance_variable_get(:@think) == :sub_remove
|
|
1389
|
+
return unless ent.think_time.positive?
|
|
1390
|
+
|
|
1391
|
+
ent.think_time = [ent.think_time - dt, 0.0].max
|
|
1392
|
+
remove_entity(ent) if ent.think_time <= 0.000001
|
|
1393
|
+
end
|
|
1394
|
+
|
|
1395
|
+
def schedule_remove(ent, delay)
|
|
1396
|
+
ent.instance_variable_set(:@think, :sub_remove)
|
|
1397
|
+
ent.think_time = delay
|
|
1398
|
+
end
|
|
1399
|
+
|
|
1400
|
+
def update_delayed_uses(dt)
|
|
1401
|
+
return if @delayed_uses.empty?
|
|
1402
|
+
|
|
1403
|
+
ready, waiting = @delayed_uses.partition do |delayed_use|
|
|
1404
|
+
delayed_use[:remaining] -= dt
|
|
1405
|
+
delayed_use[:remaining] <= 0.000001
|
|
1406
|
+
end
|
|
1407
|
+
@delayed_uses = waiting
|
|
1408
|
+
|
|
1409
|
+
ready.each do |delayed_use|
|
|
1410
|
+
fire_use_targets(
|
|
1411
|
+
delayed_use[:killtarget],
|
|
1412
|
+
delayed_use[:target],
|
|
1413
|
+
activator: delayed_use[:activator],
|
|
1414
|
+
message: delayed_use[:message],
|
|
1415
|
+
source_noise: nil
|
|
1416
|
+
)
|
|
1417
|
+
end
|
|
1418
|
+
end
|
|
1419
|
+
|
|
1420
|
+
def use_door_targets(ent, activator:)
|
|
1421
|
+
delay = (ent["delay"] || "0").to_f
|
|
1422
|
+
unless delay.zero?
|
|
1423
|
+
@delayed_uses << {
|
|
1424
|
+
remaining: delay,
|
|
1425
|
+
message: nil,
|
|
1426
|
+
killtarget: ent.killtarget,
|
|
1427
|
+
target: ent.target,
|
|
1428
|
+
activator: activator,
|
|
1429
|
+
source_noise: nil
|
|
1430
|
+
}
|
|
1431
|
+
return
|
|
1432
|
+
end
|
|
1433
|
+
|
|
1434
|
+
fire_use_targets(ent.killtarget, ent.target, activator: activator, message: nil, source_noise: nil)
|
|
1435
|
+
end
|
|
1436
|
+
|
|
1437
|
+
def touch_onlyregistered(ent, activator:)
|
|
1438
|
+
return false unless activator&.classname == "player"
|
|
1439
|
+
return true if ent.instance_variable_get(:@attack_finished).to_f > @time
|
|
1440
|
+
|
|
1441
|
+
ent.instance_variable_set(:@attack_finished, @time + 2.0)
|
|
1442
|
+
unless @registered
|
|
1443
|
+
unless ent.message.nil? || ent.message.empty?
|
|
1444
|
+
centerprint_use_message(activator, ent.message, source_noise: ent.instance_variable_get(:@noise))
|
|
1445
|
+
play_trigger_sound(ent)
|
|
1446
|
+
end
|
|
1447
|
+
return true
|
|
1448
|
+
end
|
|
1449
|
+
|
|
1450
|
+
ent.message = ""
|
|
1451
|
+
use_targets(ent, activator: activator)
|
|
1452
|
+
remove_entity(ent)
|
|
1453
|
+
true
|
|
1454
|
+
end
|
|
1455
|
+
|
|
1456
|
+
def touch_monsterjump(ent, activator)
|
|
1457
|
+
return false unless activator
|
|
1458
|
+
return false unless (activator.flags & (FL_MONSTER | FL_FLY | FL_SWIM)) == FL_MONSTER
|
|
1459
|
+
|
|
1460
|
+
movedir = trigger_movedir(ent)
|
|
1461
|
+
activator.velocity = Math::Vec3.new(
|
|
1462
|
+
movedir.x * ent.speed,
|
|
1463
|
+
movedir.y * ent.speed,
|
|
1464
|
+
activator.velocity.z
|
|
1465
|
+
)
|
|
1466
|
+
|
|
1467
|
+
return true if (activator.flags & FL_ONGROUND).zero?
|
|
1468
|
+
|
|
1469
|
+
activator.flags -= FL_ONGROUND
|
|
1470
|
+
activator.velocity = Math::Vec3.new(
|
|
1471
|
+
activator.velocity.x,
|
|
1472
|
+
activator.velocity.y,
|
|
1473
|
+
ent.height
|
|
1474
|
+
)
|
|
1475
|
+
true
|
|
1476
|
+
end
|
|
1477
|
+
|
|
1478
|
+
def touch_push(ent, activator)
|
|
1479
|
+
pushed = false
|
|
1480
|
+
if activator&.classname == "grenade" || activator&.health&.positive?
|
|
1481
|
+
activator.velocity = trigger_movedir(ent) * ent.speed * 10.0
|
|
1482
|
+
play_push_player_sound(activator) if activator.classname == "player"
|
|
1483
|
+
pushed = true
|
|
1484
|
+
end
|
|
1485
|
+
remove_entity(ent) if (ent.spawnflags & 1) != 0
|
|
1486
|
+
pushed
|
|
1487
|
+
end
|
|
1488
|
+
|
|
1489
|
+
def play_push_player_sound(activator)
|
|
1490
|
+
return unless activator.instance_variable_get(:@fly_sound).to_f < @time
|
|
1491
|
+
|
|
1492
|
+
activator.instance_variable_set(:@fly_sound, @time + 1.5)
|
|
1493
|
+
@sound_events&.on_trigger_push_player
|
|
1494
|
+
end
|
|
1495
|
+
|
|
1496
|
+
def touch_hurt(ent, activator)
|
|
1497
|
+
return false unless activator_takedamage?(activator)
|
|
1498
|
+
|
|
1499
|
+
ent.instance_variable_set(:@solid, 0)
|
|
1500
|
+
damage_takedamage_activator(activator, (ent["dmg"] || "5").to_f)
|
|
1501
|
+
ent.instance_variable_set(:@think, :hurt_on)
|
|
1502
|
+
ent.instance_variable_set(:@touch_disabled, true)
|
|
1503
|
+
ent.think_time = 1.0
|
|
1504
|
+
true
|
|
1505
|
+
end
|
|
1506
|
+
|
|
1507
|
+
def touch_secret_trigger(ent, activator:)
|
|
1508
|
+
return false unless activator&.classname == "player"
|
|
1509
|
+
|
|
1510
|
+
ent.instance_variable_set(:@takedamage, 0)
|
|
1511
|
+
@found_secrets += 1
|
|
1512
|
+
play_trigger_sound(ent)
|
|
1513
|
+
use_targets(ent, activator: activator)
|
|
1514
|
+
ent.instance_variable_set(:@touch_disabled, true)
|
|
1515
|
+
schedule_remove(ent, 0.1)
|
|
1516
|
+
true
|
|
1517
|
+
end
|
|
1518
|
+
|
|
1519
|
+
def play_trigger_sound(ent)
|
|
1520
|
+
return unless ent.instance_variable_get(:@noise)
|
|
1521
|
+
return unless @sound_events
|
|
1522
|
+
|
|
1523
|
+
if @sound_events.respond_to?(:on_trigger)
|
|
1524
|
+
@sound_events.on_trigger(ent)
|
|
1525
|
+
elsif @sound_events.respond_to?(:on_trigger_secret)
|
|
1526
|
+
@sound_events.on_trigger_secret(ent)
|
|
1527
|
+
end
|
|
1528
|
+
end
|
|
1529
|
+
|
|
1530
|
+
def play_brush_noise(ent, ivar)
|
|
1531
|
+
sound = ent.instance_variable_get(ivar)
|
|
1532
|
+
return if sound.nil? || sound.empty?
|
|
1533
|
+
return if sound == "misc/null.wav"
|
|
1534
|
+
return unless @sound_events&.respond_to?(:on_trigger)
|
|
1535
|
+
|
|
1536
|
+
event = Object.new
|
|
1537
|
+
event.instance_variable_set(:@noise, sound)
|
|
1538
|
+
@sound_events.on_trigger(event)
|
|
1539
|
+
end
|
|
1540
|
+
|
|
1541
|
+
def trigger_cooldown_active?(ent)
|
|
1542
|
+
["trigger_multiple", "trigger_hurt"].include?(ent.classname) && ent.think_time.positive?
|
|
1543
|
+
end
|
|
1544
|
+
|
|
1545
|
+
def multi_trigger?(ent)
|
|
1546
|
+
["trigger_once", "trigger_multiple", "trigger_secret"].include?(ent.classname)
|
|
1547
|
+
end
|
|
1548
|
+
|
|
1549
|
+
def multi_trigger_waiting?(ent)
|
|
1550
|
+
return false unless multi_trigger?(ent)
|
|
1551
|
+
|
|
1552
|
+
ent.think_time.positive?
|
|
1553
|
+
end
|
|
1554
|
+
|
|
1555
|
+
def notouch_trigger?(ent)
|
|
1556
|
+
multi_trigger?(ent) && (ent.spawnflags & 1) != 0
|
|
1557
|
+
end
|
|
1558
|
+
|
|
1559
|
+
def trigger_facing_allowed?(ent, player_forward)
|
|
1560
|
+
return true unless multi_trigger?(ent)
|
|
1561
|
+
return true unless player_forward
|
|
1562
|
+
return true unless trigger_has_angle_restriction?(ent)
|
|
1563
|
+
|
|
1564
|
+
player_forward.dot(trigger_movedir(ent)) >= 0.0
|
|
1565
|
+
end
|
|
1566
|
+
|
|
1567
|
+
def trigger_touch_facing_allowed?(ent, activator)
|
|
1568
|
+
trigger_facing_allowed?(ent, activator_forward_vector(activator))
|
|
1569
|
+
end
|
|
1570
|
+
|
|
1571
|
+
def activator_forward_vector(activator)
|
|
1572
|
+
return activator.forward_flat if activator.respond_to?(:forward_flat)
|
|
1573
|
+
return activator.forward_vector if activator.respond_to?(:forward_vector)
|
|
1574
|
+
|
|
1575
|
+
nil
|
|
1576
|
+
end
|
|
1577
|
+
|
|
1578
|
+
def trigger_has_angle_restriction?(ent)
|
|
1579
|
+
return true if ent.move_dir != Math::Vec3::ORIGIN
|
|
1580
|
+
return true if ent["angles"] && ent["angles"].split.map(&:to_f) != [0.0, 0.0, 0.0]
|
|
1581
|
+
return false unless ent["angle"]
|
|
1582
|
+
|
|
1583
|
+
ent["angle"].to_f != 0.0
|
|
1584
|
+
end
|
|
1585
|
+
|
|
1586
|
+
def health_trigger?(ent)
|
|
1587
|
+
multi_trigger?(ent) && ent.health.positive?
|
|
1588
|
+
end
|
|
1589
|
+
|
|
1590
|
+
def restore_health_trigger(ent)
|
|
1591
|
+
return unless ent.classname == "trigger_multiple"
|
|
1592
|
+
max_health = ent.instance_variable_get(:@max_health)
|
|
1593
|
+
return unless max_health
|
|
1594
|
+
return unless ent.health <= 0.0
|
|
1595
|
+
|
|
1596
|
+
ent.health = max_health
|
|
1597
|
+
ent.instance_variable_set(:@takedamage, 1)
|
|
1598
|
+
ent.instance_variable_set(:@solid, 2)
|
|
1599
|
+
end
|
|
1600
|
+
|
|
1601
|
+
def shootable_button?(ent)
|
|
1602
|
+
ent.classname == "func_button" && ent.instance_variable_get(:@max_health).to_f.positive?
|
|
1603
|
+
end
|
|
1604
|
+
|
|
1605
|
+
def shootable_door?(ent)
|
|
1606
|
+
regular_door?(ent) && ent.instance_variable_get(:@max_health).to_f.positive?
|
|
1607
|
+
end
|
|
1608
|
+
|
|
1609
|
+
def toggle_door?(ent)
|
|
1610
|
+
regular_door?(ent) && (ent.spawnflags & DOOR_TOGGLE) != 0
|
|
1611
|
+
end
|
|
1612
|
+
|
|
1613
|
+
def touch_activated_door?(ent)
|
|
1614
|
+
regular_door?(ent) && !linked_door_targetname?(ent) && !linked_door_shootable?(ent)
|
|
1615
|
+
end
|
|
1616
|
+
|
|
1617
|
+
def linked_door_keyed?(ent)
|
|
1618
|
+
linked_door_group(ent).any? { |door| door.instance_variable_get(:@required_key) }
|
|
1619
|
+
end
|
|
1620
|
+
|
|
1621
|
+
def regular_door?(ent)
|
|
1622
|
+
ent.classname == "func_door" || ent.instance_variable_get(:@regular_door)
|
|
1623
|
+
end
|
|
1624
|
+
|
|
1625
|
+
def linked_door_targetname?(ent)
|
|
1626
|
+
linked_door_group(ent).any? { |door| door.targetname }
|
|
1627
|
+
end
|
|
1628
|
+
|
|
1629
|
+
def linked_door_shootable?(ent)
|
|
1630
|
+
linked_door_group(ent).any? { |door| shootable_door?(door) }
|
|
1631
|
+
end
|
|
1632
|
+
|
|
1633
|
+
def linked_door_group(ent)
|
|
1634
|
+
ent.instance_variable_get(:@door_group) || [ent]
|
|
1635
|
+
end
|
|
1636
|
+
|
|
1637
|
+
def linked_door_owner(ent)
|
|
1638
|
+
ent.instance_variable_get(:@owner) || ent
|
|
1639
|
+
end
|
|
1640
|
+
|
|
1641
|
+
def damage_button(ent, amount, activator: nil)
|
|
1642
|
+
return false unless shootable_button?(ent)
|
|
1643
|
+
return true if ent.instance_variable_get(:@shot_disabled)
|
|
1644
|
+
|
|
1645
|
+
ent.health -= amount.to_f.ceil
|
|
1646
|
+
return true if ent.health.positive?
|
|
1647
|
+
|
|
1648
|
+
ent.health = ent.instance_variable_get(:@max_health)
|
|
1649
|
+
ent.instance_variable_set(:@shot_disabled, true)
|
|
1650
|
+
ent.instance_variable_set(:@takedamage, 0)
|
|
1651
|
+
ent.instance_variable_set(:@button_activator, activator) if activator
|
|
1652
|
+
fire_button(ent)
|
|
1653
|
+
ent.instance_variable_set(:@move_fraction, 0.0)
|
|
1654
|
+
true
|
|
1655
|
+
end
|
|
1656
|
+
|
|
1657
|
+
def damage_door(ent, amount, activator: nil)
|
|
1658
|
+
return false unless shootable_door?(ent)
|
|
1659
|
+
return true if ent.instance_variable_get(:@shot_disabled)
|
|
1660
|
+
|
|
1661
|
+
ent.health -= amount.to_f.ceil
|
|
1662
|
+
return true if ent.health.positive?
|
|
1663
|
+
|
|
1664
|
+
ent.health = ent.instance_variable_get(:@max_health)
|
|
1665
|
+
ent.instance_variable_set(:@shot_disabled, true)
|
|
1666
|
+
ent.instance_variable_set(:@takedamage, 0)
|
|
1667
|
+
open_linked_doors_now(ent, activator: activator || ent)
|
|
1668
|
+
true
|
|
1669
|
+
end
|
|
1670
|
+
|
|
1671
|
+
def damage_secret_door(ent, activator:)
|
|
1672
|
+
return false unless secret_door_shootable?(ent)
|
|
1673
|
+
return false if ent.instance_variable_get(:@takedamage).to_i.zero?
|
|
1674
|
+
|
|
1675
|
+
use_secret_door(ent, activator: activator || ent)
|
|
1676
|
+
true
|
|
1677
|
+
end
|
|
1678
|
+
|
|
1679
|
+
def update_secret_door(ent, player_pos, player_state = nil)
|
|
1680
|
+
if ent.instance_variable_get(:@triggered)
|
|
1681
|
+
ent.instance_variable_set(:@triggered, false)
|
|
1682
|
+
use_secret_door(ent, activator: ent)
|
|
1683
|
+
end
|
|
1684
|
+
|
|
1685
|
+
case ent.state
|
|
1686
|
+
when STATE_IDLE, STATE_CLOSED
|
|
1687
|
+
touch_door_message(ent, player_state) if near_entity?(ent, player_pos, 128.0)
|
|
1688
|
+
when STATE_OPENING
|
|
1689
|
+
move_toward(ent, ent.instance_variable_get(:@open_pos), @time_step)
|
|
1690
|
+
secret_door_blocked(ent, player_pos, player_state)
|
|
1691
|
+
advance_secret_door(ent) if ent.instance_variable_get(:@move_fraction) >= 1.0
|
|
1692
|
+
when :secret_pause1
|
|
1693
|
+
ent.think_time -= @time_step
|
|
1694
|
+
start_secret_move(ent, :move2, ent.instance_variable_get(:@secret_dest2)) if ent.think_time <= 0.0
|
|
1695
|
+
when STATE_OPEN
|
|
1696
|
+
return if (ent.spawnflags & 1) != 0
|
|
1697
|
+
|
|
1698
|
+
ent.think_time -= @time_step
|
|
1699
|
+
start_secret_move(ent, :move3, ent.instance_variable_get(:@secret_dest1)) if ent.think_time <= 0.0
|
|
1700
|
+
when :secret_pause2
|
|
1701
|
+
ent.think_time -= @time_step
|
|
1702
|
+
start_secret_move(ent, :move4, ent.instance_variable_get(:@oldorigin)) if ent.think_time <= 0.0
|
|
1703
|
+
end
|
|
1704
|
+
end
|
|
1705
|
+
|
|
1706
|
+
def use_secret_door(ent, activator:)
|
|
1707
|
+
return if ent.position != ent.instance_variable_get(:@oldorigin)
|
|
1708
|
+
|
|
1709
|
+
ent.health = 10_000.0
|
|
1710
|
+
ent.message = nil
|
|
1711
|
+
use_targets(ent, activator: activator)
|
|
1712
|
+
unless (ent.spawnflags & SECRET_NO_SHOOT) != 0
|
|
1713
|
+
ent.instance_variable_set(:@shot_disabled, true)
|
|
1714
|
+
ent.instance_variable_set(:@takedamage, 0)
|
|
1715
|
+
ent.instance_variable_set(:@th_pain, :sub_null)
|
|
1716
|
+
end
|
|
1717
|
+
calculate_secret_door_destinations(ent)
|
|
1718
|
+
play_brush_noise(ent, :@noise1)
|
|
1719
|
+
start_secret_move(ent, :move1, ent.instance_variable_get(:@secret_dest1))
|
|
1720
|
+
end
|
|
1721
|
+
|
|
1722
|
+
def secret_door_blocked(ent, player_pos, player_state)
|
|
1723
|
+
return unless player_state&.alive?
|
|
1724
|
+
return unless player_blocking_brush?(ent, player_pos)
|
|
1725
|
+
return if ent.instance_variable_get(:@attack_finished).to_f > @time
|
|
1726
|
+
|
|
1727
|
+
ent.instance_variable_set(:@attack_finished, @time + 0.5)
|
|
1728
|
+
player_state.deathtype = "squish" if player_state.respond_to?(:deathtype=)
|
|
1729
|
+
player_state.take_damage((ent["dmg"] || "2").to_f)
|
|
1730
|
+
end
|
|
1731
|
+
|
|
1732
|
+
def calculate_secret_door_destinations(ent)
|
|
1733
|
+
return if ent.instance_variable_get(:@secret_dest1)
|
|
1734
|
+
|
|
1735
|
+
forward, right, up = secret_door_vectors(ent)
|
|
1736
|
+
size = ent.instance_variable_get(:@secret_size)
|
|
1737
|
+
width = ent["t_width"]&.to_f
|
|
1738
|
+
width = nil if width&.zero?
|
|
1739
|
+
width ||= if (ent.spawnflags & 4) != 0
|
|
1740
|
+
up.dot(size).abs
|
|
1741
|
+
else
|
|
1742
|
+
right.dot(size).abs
|
|
1743
|
+
end
|
|
1744
|
+
length = ent["t_length"]&.to_f
|
|
1745
|
+
length = nil if length&.zero?
|
|
1746
|
+
length ||= forward.dot(size).abs
|
|
1747
|
+
|
|
1748
|
+
left_sign = (ent.spawnflags & 2) != 0 ? -1.0 : 1.0
|
|
1749
|
+
dest1 = if (ent.spawnflags & 4) != 0
|
|
1750
|
+
ent.position - up * width
|
|
1751
|
+
else
|
|
1752
|
+
ent.position + right * (width * left_sign)
|
|
1753
|
+
end
|
|
1754
|
+
ent.instance_variable_set(:@secret_dest1, dest1)
|
|
1755
|
+
ent.instance_variable_set(:@secret_dest2, dest1 + forward * length)
|
|
1756
|
+
end
|
|
1757
|
+
|
|
1758
|
+
def secret_door_vectors(ent)
|
|
1759
|
+
mangle = ent.instance_variable_get(:@mangle)
|
|
1760
|
+
yaw = if mangle && mangle != Math::Vec3::ORIGIN
|
|
1761
|
+
mangle.y
|
|
1762
|
+
elsif ent.angles != Math::Vec3::ORIGIN
|
|
1763
|
+
ent.angles.y
|
|
1764
|
+
else
|
|
1765
|
+
ent.angle
|
|
1766
|
+
end
|
|
1767
|
+
rad = yaw * ::Math::PI / 180.0
|
|
1768
|
+
forward = Math::Vec3.new(::Math.cos(rad), ::Math.sin(rad), 0.0)
|
|
1769
|
+
right_rad = (yaw - 90.0) * ::Math::PI / 180.0
|
|
1770
|
+
right = Math::Vec3.new(::Math.cos(right_rad), ::Math.sin(right_rad), 0.0)
|
|
1771
|
+
[forward, right, Math::Vec3.new(0.0, 0.0, 1.0)]
|
|
1772
|
+
end
|
|
1773
|
+
|
|
1774
|
+
def start_secret_move(ent, phase, target)
|
|
1775
|
+
ent.instance_variable_set(:@secret_phase, phase)
|
|
1776
|
+
ent.instance_variable_set(:@open_pos, target)
|
|
1777
|
+
ent.instance_variable_set(:@move_fraction, 0.0)
|
|
1778
|
+
play_brush_noise(ent, :@noise2)
|
|
1779
|
+
ent.state = STATE_OPENING
|
|
1780
|
+
end
|
|
1781
|
+
|
|
1782
|
+
def advance_secret_door(ent)
|
|
1783
|
+
case ent.instance_variable_get(:@secret_phase)
|
|
1784
|
+
when :move1
|
|
1785
|
+
ent.state = :secret_pause1
|
|
1786
|
+
ent.think_time = 1.0
|
|
1787
|
+
play_brush_noise(ent, :@noise3)
|
|
1788
|
+
when :move2
|
|
1789
|
+
ent.state = STATE_OPEN
|
|
1790
|
+
ent.think_time = ent.wait
|
|
1791
|
+
play_brush_noise(ent, :@noise3)
|
|
1792
|
+
when :move3
|
|
1793
|
+
ent.state = :secret_pause2
|
|
1794
|
+
ent.think_time = 1.0
|
|
1795
|
+
play_brush_noise(ent, :@noise3)
|
|
1796
|
+
when :move4
|
|
1797
|
+
ent.state = STATE_CLOSED
|
|
1798
|
+
if secret_door_shootable?(ent)
|
|
1799
|
+
ent.health = 10_000.0
|
|
1800
|
+
ent.instance_variable_set(:@takedamage, 1)
|
|
1801
|
+
ent.instance_variable_set(:@th_pain, :fd_secret_use)
|
|
1802
|
+
ent.instance_variable_set(:@th_die, :fd_secret_use)
|
|
1803
|
+
ent.instance_variable_set(:@shot_disabled, false)
|
|
1804
|
+
end
|
|
1805
|
+
play_brush_noise(ent, :@noise3)
|
|
1806
|
+
end
|
|
1807
|
+
end
|
|
1808
|
+
|
|
1809
|
+
def secret_door?(ent)
|
|
1810
|
+
ent.classname == "func_door_secret" || ent.instance_variable_get(:@secret_door)
|
|
1811
|
+
end
|
|
1812
|
+
|
|
1813
|
+
def secret_door_shootable?(ent)
|
|
1814
|
+
!ent.targetname || (ent.spawnflags & SECRET_YES_SHOOT) != 0
|
|
1815
|
+
end
|
|
1816
|
+
|
|
1817
|
+
def remove_killtargets(target_name)
|
|
1818
|
+
@entities.each do |ent|
|
|
1819
|
+
next unless ent.targetname == target_name
|
|
1820
|
+
|
|
1821
|
+
remove_entity(ent)
|
|
1822
|
+
end
|
|
1823
|
+
end
|
|
1824
|
+
|
|
1825
|
+
def remove_entity(ent)
|
|
1826
|
+
ent.instance_variable_set(:@removed, true)
|
|
1827
|
+
ent.instance_variable_set(:@touch_disabled, true)
|
|
1828
|
+
end
|
|
1829
|
+
|
|
1830
|
+
def fire_use_targets(killtarget, target, activator:, message: nil, source_noise: nil)
|
|
1831
|
+
centerprint_use_message(activator, message, source_noise: source_noise)
|
|
1832
|
+
if killtarget && !killtarget.empty?
|
|
1833
|
+
remove_killtargets(killtarget)
|
|
1834
|
+
return
|
|
1835
|
+
end
|
|
1836
|
+
fire_targets(target, activator: activator) if target
|
|
1837
|
+
end
|
|
1838
|
+
|
|
1839
|
+
def centerprint_use_message(activator, message, source_noise: nil)
|
|
1840
|
+
return unless activator.respond_to?(:classname)
|
|
1841
|
+
return unless activator&.classname == "player"
|
|
1842
|
+
return if message.nil? || message.empty?
|
|
1843
|
+
|
|
1844
|
+
activator.instance_variable_set(:@centerprint, message)
|
|
1845
|
+
play_default_message_sound if source_noise.nil? || source_noise.empty?
|
|
1846
|
+
end
|
|
1847
|
+
|
|
1848
|
+
def play_default_message_sound
|
|
1849
|
+
return unless @sound_events&.respond_to?(:on_trigger)
|
|
1850
|
+
|
|
1851
|
+
entity = Object.new
|
|
1852
|
+
entity.instance_variable_set(:@noise, "misc/talk.wav")
|
|
1853
|
+
@sound_events.on_trigger(entity)
|
|
1854
|
+
end
|
|
1855
|
+
|
|
1856
|
+
def fire_targets(target_name, activator:)
|
|
1857
|
+
return unless target_name
|
|
1858
|
+
|
|
1859
|
+
targets = @target_map[target_name]
|
|
1860
|
+
return unless targets
|
|
1861
|
+
|
|
1862
|
+
targets.each do |ent|
|
|
1863
|
+
next if ent.instance_variable_get(:@removed)
|
|
1864
|
+
|
|
1865
|
+
if ent.classname == "trigger_counter"
|
|
1866
|
+
use_counter(ent, activator: activator)
|
|
1867
|
+
next
|
|
1868
|
+
end
|
|
1869
|
+
|
|
1870
|
+
if ent.classname == "trigger_relay"
|
|
1871
|
+
use_targets(ent, activator: activator)
|
|
1872
|
+
next
|
|
1873
|
+
end
|
|
1874
|
+
|
|
1875
|
+
if multi_trigger?(ent)
|
|
1876
|
+
activate_multi_trigger(ent, activator: activator)
|
|
1877
|
+
next
|
|
1878
|
+
end
|
|
1879
|
+
|
|
1880
|
+
if ent.classname == "trigger_teleport"
|
|
1881
|
+
ent.think_time = @time + 0.2
|
|
1882
|
+
ent.instance_variable_set(:@think, :sub_null)
|
|
1883
|
+
next
|
|
1884
|
+
end
|
|
1885
|
+
|
|
1886
|
+
if toggle_frame_bmodel?(ent)
|
|
1887
|
+
ent.frame = 1 - ent.frame
|
|
1888
|
+
next
|
|
1889
|
+
end
|
|
1890
|
+
|
|
1891
|
+
if toggle_light?(ent)
|
|
1892
|
+
toggle_light(ent)
|
|
1893
|
+
next
|
|
1894
|
+
end
|
|
1895
|
+
|
|
1896
|
+
if ent.classname == "func_button"
|
|
1897
|
+
use_button(ent, activator: activator)
|
|
1898
|
+
next
|
|
1899
|
+
end
|
|
1900
|
+
|
|
1901
|
+
if regular_door?(ent)
|
|
1902
|
+
use_door(ent, activator: activator)
|
|
1903
|
+
next
|
|
1904
|
+
end
|
|
1905
|
+
|
|
1906
|
+
if secret_door?(ent)
|
|
1907
|
+
use_secret_door(ent, activator: activator)
|
|
1908
|
+
next
|
|
1909
|
+
end
|
|
1910
|
+
|
|
1911
|
+
if train_entity?(ent)
|
|
1912
|
+
use_train(ent)
|
|
1913
|
+
next
|
|
1914
|
+
end
|
|
1915
|
+
|
|
1916
|
+
if platform_entity?(ent)
|
|
1917
|
+
use_platform(ent)
|
|
1918
|
+
next
|
|
1919
|
+
end
|
|
1920
|
+
end
|
|
1921
|
+
end
|
|
1922
|
+
|
|
1923
|
+
def use_door(ent, activator:)
|
|
1924
|
+
owner = linked_door_owner(ent)
|
|
1925
|
+
play_brush_noise(owner, :@noise4) if owner.instance_variable_get(:@required_key)
|
|
1926
|
+
if toggle_door?(owner) && [STATE_OPENING, STATE_OPEN].include?(owner.state)
|
|
1927
|
+
linked_door_group(owner).each do |member|
|
|
1928
|
+
member.instance_variable_set(:@triggered, false)
|
|
1929
|
+
member.instance_variable_set(:@trigger_activator, nil)
|
|
1930
|
+
member.instance_variable_set(:@move_fraction, 0.0)
|
|
1931
|
+
member.instance_variable_set(:@activated_at_time, @time)
|
|
1932
|
+
play_brush_noise(member, :@noise2)
|
|
1933
|
+
member.state = STATE_CLOSING
|
|
1934
|
+
end
|
|
1935
|
+
clear_linked_door_messages(owner)
|
|
1936
|
+
return
|
|
1937
|
+
end
|
|
1938
|
+
|
|
1939
|
+
linked_door_group(owner).each do |member|
|
|
1940
|
+
member.instance_variable_set(:@triggered, false)
|
|
1941
|
+
member.instance_variable_set(:@trigger_activator, activator)
|
|
1942
|
+
if member.state == STATE_OPENING
|
|
1943
|
+
next
|
|
1944
|
+
elsif member.state == STATE_OPEN
|
|
1945
|
+
member.think_time = member.wait
|
|
1946
|
+
next
|
|
1947
|
+
end
|
|
1948
|
+
|
|
1949
|
+
member.instance_variable_set(:@move_fraction, 0.0)
|
|
1950
|
+
member.instance_variable_set(:@activated_at_time, @time)
|
|
1951
|
+
play_brush_noise(member, :@noise2)
|
|
1952
|
+
member.state = STATE_OPENING
|
|
1953
|
+
|
|
1954
|
+
use_door_targets(member, activator: activator)
|
|
1955
|
+
end
|
|
1956
|
+
clear_linked_door_messages(owner)
|
|
1957
|
+
end
|
|
1958
|
+
|
|
1959
|
+
def train_entity?(ent)
|
|
1960
|
+
["train", "misc_teleporttrain"].include?(ent.classname)
|
|
1961
|
+
end
|
|
1962
|
+
|
|
1963
|
+
def platform_entity?(ent)
|
|
1964
|
+
ent.classname == "plat" || ent.classname == "func_plat"
|
|
1965
|
+
end
|
|
1966
|
+
|
|
1967
|
+
def use_train(ent)
|
|
1968
|
+
return unless ent.state == STATE_IDLE
|
|
1969
|
+
|
|
1970
|
+
ent.instance_variable_set(:@triggered, false)
|
|
1971
|
+
start_train_next(ent)
|
|
1972
|
+
end
|
|
1973
|
+
|
|
1974
|
+
def use_platform(ent)
|
|
1975
|
+
return if ent.instance_variable_get(:@use) == :sub_null
|
|
1976
|
+
|
|
1977
|
+
if ent.instance_variable_get(:@targeted_platform)
|
|
1978
|
+
ent.instance_variable_set(:@use, :sub_null)
|
|
1979
|
+
raise "plat_use: not in up state" unless ent.state == STATE_IDLE
|
|
1980
|
+
|
|
1981
|
+
ent.instance_variable_set(:@platform_used, true)
|
|
1982
|
+
ent.instance_variable_set(:@triggered, false)
|
|
1983
|
+
ent.instance_variable_set(:@move_fraction, 0.0)
|
|
1984
|
+
ent.instance_variable_set(:@activated_at_time, @time)
|
|
1985
|
+
play_brush_noise(ent, :@noise)
|
|
1986
|
+
ent.state = STATE_OPENING
|
|
1987
|
+
return
|
|
1988
|
+
end
|
|
1989
|
+
|
|
1990
|
+
return unless ent.state == STATE_IDLE || ent.state == STATE_CLOSED
|
|
1991
|
+
|
|
1992
|
+
ent.instance_variable_set(:@triggered, false)
|
|
1993
|
+
ent.instance_variable_set(:@move_fraction, 0.0)
|
|
1994
|
+
ent.instance_variable_set(:@activated_at_time, @time)
|
|
1995
|
+
play_brush_noise(ent, :@noise)
|
|
1996
|
+
ent.state = STATE_CLOSING
|
|
1997
|
+
end
|
|
1998
|
+
|
|
1999
|
+
def open_linked_doors_now(ent, activator:)
|
|
2000
|
+
linked_door_group(ent).each do |member|
|
|
2001
|
+
next if [STATE_OPENING, STATE_OPEN].include?(member.state)
|
|
2002
|
+
|
|
2003
|
+
member.instance_variable_set(:@trigger_activator, activator)
|
|
2004
|
+
member.instance_variable_set(:@move_fraction, 0.0)
|
|
2005
|
+
member.instance_variable_set(:@activated_at_time, @time)
|
|
2006
|
+
play_brush_noise(member, :@noise2)
|
|
2007
|
+
member.state = STATE_OPENING
|
|
2008
|
+
use_door_targets(member, activator: activator)
|
|
2009
|
+
end
|
|
2010
|
+
clear_linked_door_messages(ent)
|
|
2011
|
+
end
|
|
2012
|
+
|
|
2013
|
+
def clear_linked_door_messages(ent)
|
|
2014
|
+
linked_door_group(ent).each { |member| member.message = nil }
|
|
2015
|
+
end
|
|
2016
|
+
|
|
2017
|
+
def toggle_frame_bmodel?(ent)
|
|
2018
|
+
ent.instance_variable_get(:@use) == :func_wall_use
|
|
2019
|
+
end
|
|
2020
|
+
|
|
2021
|
+
def toggle_light?(ent)
|
|
2022
|
+
["light", "light_fluoro"].include?(ent.classname) && ent.style >= 32
|
|
2023
|
+
end
|
|
2024
|
+
|
|
2025
|
+
def toggle_light(ent)
|
|
2026
|
+
ent.instance_variable_set(:@use, :light_use)
|
|
2027
|
+
if (ent.spawnflags & 1) != 0
|
|
2028
|
+
ent.lightstyle = "m"
|
|
2029
|
+
ent.spawnflags -= 1
|
|
2030
|
+
else
|
|
2031
|
+
ent.lightstyle = "a"
|
|
2032
|
+
ent.spawnflags += 1
|
|
2033
|
+
end
|
|
2034
|
+
end
|
|
2035
|
+
|
|
2036
|
+
def use_button(ent, activator:)
|
|
2037
|
+
return if [STATE_OPENING, STATE_OPEN].include?(ent.state)
|
|
2038
|
+
|
|
2039
|
+
ent.instance_variable_set(:@button_activator, activator)
|
|
2040
|
+
ent.instance_variable_set(:@triggered, false)
|
|
2041
|
+
ent.instance_variable_set(:@move_fraction, 0.0)
|
|
2042
|
+
fire_button(ent)
|
|
2043
|
+
end
|
|
2044
|
+
|
|
2045
|
+
def fire_button(ent)
|
|
2046
|
+
return if [STATE_OPENING, STATE_OPEN].include?(ent.state)
|
|
2047
|
+
|
|
2048
|
+
play_trigger_sound(ent)
|
|
2049
|
+
ent.state = STATE_OPENING
|
|
2050
|
+
end
|
|
2051
|
+
|
|
2052
|
+
def use_counter(ent, activator:)
|
|
2053
|
+
ent.count -= 1
|
|
2054
|
+
return if ent.count.negative?
|
|
2055
|
+
unless ent.count.zero?
|
|
2056
|
+
centerprint_counter_message(ent, activator, counter_remaining_message(ent.count))
|
|
2057
|
+
return
|
|
2058
|
+
end
|
|
2059
|
+
|
|
2060
|
+
centerprint_counter_message(ent, activator, "Sequence completed!")
|
|
2061
|
+
ent.instance_variable_set(:@enemy, activator)
|
|
2062
|
+
activate_multi_trigger(ent, activator: activator)
|
|
2063
|
+
end
|
|
2064
|
+
|
|
2065
|
+
def centerprint_counter_message(ent, activator, message)
|
|
2066
|
+
return unless activator&.classname == "player"
|
|
2067
|
+
return if (ent.spawnflags & 1) != 0
|
|
2068
|
+
|
|
2069
|
+
activator.instance_variable_set(:@centerprint, message)
|
|
2070
|
+
end
|
|
2071
|
+
|
|
2072
|
+
def trigger_movedir(ent)
|
|
2073
|
+
return ent.move_dir if ent.move_dir != Math::Vec3::ORIGIN
|
|
2074
|
+
return ent.forward_vector if ent["angles"] && ent["angles"].split.map(&:to_f) != [0.0, 0.0, 0.0]
|
|
2075
|
+
return ent.forward_vector if ent["angle"] && ent["angle"].to_f != 0.0
|
|
2076
|
+
|
|
2077
|
+
Math::Vec3::ORIGIN
|
|
2078
|
+
end
|
|
2079
|
+
|
|
2080
|
+
def trigger_model_index(ent)
|
|
2081
|
+
ent.instance_variable_get(:@trigger_model_index)
|
|
2082
|
+
end
|
|
2083
|
+
|
|
2084
|
+
def activator_takedamage?(activator)
|
|
2085
|
+
activator && activator.instance_variable_get(:@takedamage).to_i != 0
|
|
2086
|
+
end
|
|
2087
|
+
|
|
2088
|
+
def damage_takedamage_activator(activator, damage)
|
|
2089
|
+
if activator.respond_to?(:damageable?) && activator.damageable?
|
|
2090
|
+
activator.take_damage(damage)
|
|
2091
|
+
else
|
|
2092
|
+
activator.health -= damage.to_f.ceil if activator.respond_to?(:health) && activator.respond_to?(:health=)
|
|
2093
|
+
end
|
|
2094
|
+
end
|
|
2095
|
+
|
|
2096
|
+
def init_trigger_class?(ent)
|
|
2097
|
+
ent.classname.start_with?("trigger_")
|
|
2098
|
+
end
|
|
2099
|
+
|
|
2100
|
+
def counter_remaining_message(count)
|
|
2101
|
+
case count
|
|
2102
|
+
when 3 then "Only 3 more to go..."
|
|
2103
|
+
when 2 then "Only 2 more to go..."
|
|
2104
|
+
when 1 then "Only 1 more to go..."
|
|
2105
|
+
else "There are more to go..."
|
|
357
2106
|
end
|
|
358
2107
|
end
|
|
359
2108
|
end
|