quake-rb 0.1.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 +7 -0
- data/bin/quake +143 -0
- data/bin/quake-debug +83 -0
- data/lib/quake/bsp/face_vertices.rb +63 -0
- data/lib/quake/bsp/reader.rb +264 -0
- data/lib/quake/bsp/types.rb +30 -0
- data/lib/quake/bsp/vis.rb +246 -0
- data/lib/quake/camera.rb +99 -0
- data/lib/quake/debug/png_writer.rb +58 -0
- data/lib/quake/debug/screenshot.rb +26 -0
- data/lib/quake/debug/script.rb +179 -0
- data/lib/quake/entity.rb +116 -0
- data/lib/quake/game/brush_entities.rb +361 -0
- data/lib/quake/game/engine.rb +300 -0
- data/lib/quake/game/item_pickups.rb +137 -0
- data/lib/quake/game/player_state.rb +158 -0
- data/lib/quake/math/vec3.rb +35 -0
- data/lib/quake/mdl/reader.rb +176 -0
- data/lib/quake/mdl/types.rb +30 -0
- data/lib/quake/pak/reader.rb +57 -0
- data/lib/quake/pak_downloader.rb +145 -0
- data/lib/quake/palette.rb +32 -0
- data/lib/quake/physics/hull_trace.rb +193 -0
- data/lib/quake/physics/player.rb +357 -0
- data/lib/quake/renderer/gl_alias_model.rb +122 -0
- data/lib/quake/renderer/gl_brush_model.rb +162 -0
- data/lib/quake/renderer/gl_hud.rb +226 -0
- data/lib/quake/renderer/gl_lightmap.rb +261 -0
- data/lib/quake/renderer/gl_particles.rb +173 -0
- data/lib/quake/renderer/gl_sky.rb +166 -0
- data/lib/quake/renderer/gl_texture_manager.rb +54 -0
- data/lib/quake/renderer/gl_textured.rb +224 -0
- data/lib/quake/renderer/gl_viewmodel.rb +109 -0
- data/lib/quake/renderer/gl_water.rb +200 -0
- data/lib/quake/renderer/gl_wireframe.rb +36 -0
- data/lib/quake/sound/events.rb +58 -0
- data/lib/quake/sound/mixer.rb +105 -0
- data/lib/quake/version.rb +5 -0
- data/lib/quake/wad/reader.rb +69 -0
- data/lib/quake/window.rb +74 -0
- data/lib/quake.rb +19 -0
- metadata +140 -0
data/lib/quake/entity.rb
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quake
|
|
4
|
+
# Represents a parsed entity from the BSP entities lump.
|
|
5
|
+
# All Quake entities (players, monsters, items, triggers, brush models)
|
|
6
|
+
# are defined as key-value property bags in the BSP entity string.
|
|
7
|
+
class Entity
|
|
8
|
+
attr_accessor :properties, :position, :angle, :angles, :classname,
|
|
9
|
+
:model_index, :think_time, :state, :move_dir,
|
|
10
|
+
:target, :targetname, :speed, :wait, :lip, :health,
|
|
11
|
+
:sounds, :message
|
|
12
|
+
|
|
13
|
+
def initialize(properties = {})
|
|
14
|
+
@properties = properties
|
|
15
|
+
@classname = properties["classname"] || ""
|
|
16
|
+
|
|
17
|
+
# Parse common fields
|
|
18
|
+
@position = parse_vec3(properties["origin"]) || Math::Vec3::ORIGIN
|
|
19
|
+
@angle = (properties["angle"] || "0").to_f
|
|
20
|
+
@angles = parse_vec3(properties["angles"]) || Math::Vec3::ORIGIN
|
|
21
|
+
|
|
22
|
+
# Brush model reference: "*1", "*2", etc.
|
|
23
|
+
@model_index = nil
|
|
24
|
+
if (model_str = properties["model"])
|
|
25
|
+
@model_index = model_str[1..].to_i if model_str.start_with?("*")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Targeting
|
|
29
|
+
@target = properties["target"]
|
|
30
|
+
@targetname = properties["targetname"]
|
|
31
|
+
|
|
32
|
+
# Movement/behavior
|
|
33
|
+
@speed = (properties["speed"] || default_speed).to_f
|
|
34
|
+
@wait = (properties["wait"] || "3").to_f
|
|
35
|
+
@lip = (properties["lip"] || "8").to_f
|
|
36
|
+
@health = (properties["health"] || "0").to_f
|
|
37
|
+
@sounds = (properties["sounds"] || "0").to_i
|
|
38
|
+
@message = properties["message"]
|
|
39
|
+
|
|
40
|
+
# Runtime state
|
|
41
|
+
@state = :idle
|
|
42
|
+
@think_time = 0.0
|
|
43
|
+
@move_dir = Math::Vec3::ORIGIN
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def [](key) = @properties[key]
|
|
47
|
+
|
|
48
|
+
def brush_entity? = !@model_index.nil?
|
|
49
|
+
|
|
50
|
+
# Direction vector from the "angle" field (Quake convention)
|
|
51
|
+
def forward_vector
|
|
52
|
+
case @angle.to_i
|
|
53
|
+
when -1 # UP
|
|
54
|
+
Math::Vec3.new(0.0, 0.0, 1.0)
|
|
55
|
+
when -2 # DOWN
|
|
56
|
+
Math::Vec3.new(0.0, 0.0, -1.0)
|
|
57
|
+
else
|
|
58
|
+
rad = @angle * ::Math::PI / 180.0
|
|
59
|
+
Math::Vec3.new(::Math.cos(rad), ::Math.sin(rad), 0.0)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def parse_vec3(str)
|
|
66
|
+
return nil unless str
|
|
67
|
+
parts = str.split.map(&:to_f)
|
|
68
|
+
return nil unless parts.size == 3
|
|
69
|
+
Math::Vec3.new(parts[0], parts[1], parts[2])
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def default_speed
|
|
73
|
+
case @classname
|
|
74
|
+
when "func_door" then "100"
|
|
75
|
+
when "func_plat" then "150"
|
|
76
|
+
when "func_button" then "40"
|
|
77
|
+
when "func_train" then "100"
|
|
78
|
+
else "100"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Parses the BSP entity string into an array of Entity objects.
|
|
84
|
+
module EntityParser
|
|
85
|
+
def self.parse(entity_string)
|
|
86
|
+
entities = []
|
|
87
|
+
current = nil
|
|
88
|
+
|
|
89
|
+
entity_string.each_line do |line|
|
|
90
|
+
line = line.strip
|
|
91
|
+
case line
|
|
92
|
+
when "{"
|
|
93
|
+
current = {}
|
|
94
|
+
when "}"
|
|
95
|
+
entities << Entity.new(current) if current
|
|
96
|
+
current = nil
|
|
97
|
+
else
|
|
98
|
+
if current && line =~ /\A"([^"]+)"\s+"([^"]*)"\z/
|
|
99
|
+
current[$1] = $2
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
entities
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Build a lookup table: targetname -> [Entity]
|
|
108
|
+
def self.build_target_map(entities)
|
|
109
|
+
map = Hash.new { |h, k| h[k] = [] }
|
|
110
|
+
entities.each do |ent|
|
|
111
|
+
map[ent.targetname] << ent if ent.targetname
|
|
112
|
+
end
|
|
113
|
+
map
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quake
|
|
4
|
+
module Game
|
|
5
|
+
# Manages brush entity behavior: doors, platforms, buttons, triggers.
|
|
6
|
+
# Each entity has a state machine and moves between positions.
|
|
7
|
+
class BrushEntities
|
|
8
|
+
# Entity states
|
|
9
|
+
STATE_IDLE = :idle
|
|
10
|
+
STATE_OPENING = :opening
|
|
11
|
+
STATE_OPEN = :open
|
|
12
|
+
STATE_CLOSING = :closing
|
|
13
|
+
STATE_CLOSED = :closed
|
|
14
|
+
|
|
15
|
+
def initialize(entities, level, target_map)
|
|
16
|
+
@entities = entities
|
|
17
|
+
@level = level
|
|
18
|
+
@target_map = target_map
|
|
19
|
+
@brush_entities = entities.select(&:brush_entity?)
|
|
20
|
+
|
|
21
|
+
# Initialize movement data for each brush entity
|
|
22
|
+
@brush_entities.each { |ent| init_brush_entity(ent) }
|
|
23
|
+
|
|
24
|
+
puts "Initialized #{@brush_entities.size} brush entities"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Update all brush entities. Returns entities that need rendering.
|
|
28
|
+
def update(dt, player_pos)
|
|
29
|
+
# Save previous positions for platform-riding
|
|
30
|
+
@brush_entities.each do |ent|
|
|
31
|
+
ent.instance_variable_set(:@prev_pos, ent.position)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@brush_entities.each do |ent|
|
|
35
|
+
case ent.classname
|
|
36
|
+
when "func_door"
|
|
37
|
+
update_door(ent, dt, player_pos)
|
|
38
|
+
when "func_plat"
|
|
39
|
+
update_platform(ent, dt, player_pos)
|
|
40
|
+
when "func_button"
|
|
41
|
+
update_button(ent, dt, player_pos)
|
|
42
|
+
when "func_train"
|
|
43
|
+
update_train(ent, dt)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
@brush_entities
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# If the player is standing on a moving platform, return a new
|
|
51
|
+
# position snapped to the platform's top surface (so on_ground stays
|
|
52
|
+
# true and the player rides smoothly). Returns nil if not on a
|
|
53
|
+
# moving platform.
|
|
54
|
+
def snap_to_platform(player_pos)
|
|
55
|
+
@brush_entities.each do |ent|
|
|
56
|
+
next unless ent.classname == "func_plat"
|
|
57
|
+
model = @level.models[ent.model_index]
|
|
58
|
+
next unless model
|
|
59
|
+
|
|
60
|
+
prev = ent.instance_variable_get(:@prev_pos)
|
|
61
|
+
next unless prev
|
|
62
|
+
|
|
63
|
+
ent_delta_z = ent.position.z - prev.z
|
|
64
|
+
|
|
65
|
+
# Use the OLD platform position for the on_top check, since
|
|
66
|
+
# player_pos hasn't been updated for this frame's movement yet.
|
|
67
|
+
old_top_z = prev.z + model.maxs.z
|
|
68
|
+
next unless player_pos.z >= old_top_z - 8 && player_pos.z <= old_top_z + 24
|
|
69
|
+
next unless player_pos.x >= ent.position.x + model.mins.x &&
|
|
70
|
+
player_pos.x <= ent.position.x + model.maxs.x &&
|
|
71
|
+
player_pos.y >= ent.position.y + model.mins.y &&
|
|
72
|
+
player_pos.y <= ent.position.y + model.maxs.y
|
|
73
|
+
|
|
74
|
+
# Snap to the new platform top surface, slightly above so the
|
|
75
|
+
# trace-down ground check can detect it cleanly (avoid epsilon
|
|
76
|
+
# edge case at exact plane boundary).
|
|
77
|
+
new_top_z = ent.position.z + model.maxs.z + 0.5
|
|
78
|
+
return Math::Vec3.new(player_pos.x, player_pos.y, new_top_z)
|
|
79
|
+
end
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Backward-compatible delta-only method
|
|
84
|
+
def platform_motion_for(player_pos)
|
|
85
|
+
snapped = snap_to_platform(player_pos)
|
|
86
|
+
return Math::Vec3::ORIGIN unless snapped
|
|
87
|
+
snapped - player_pos
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Check if player touches any trigger entities
|
|
91
|
+
def check_triggers(player_pos, player_radius: 16.0)
|
|
92
|
+
@entities.each do |ent|
|
|
93
|
+
next unless ent.classname.start_with?("trigger_")
|
|
94
|
+
next unless ent.brush_entity?
|
|
95
|
+
|
|
96
|
+
model = @level.models[ent.model_index]
|
|
97
|
+
next unless model
|
|
98
|
+
|
|
99
|
+
# Simple AABB check against player
|
|
100
|
+
mins = model.mins
|
|
101
|
+
maxs = model.maxs
|
|
102
|
+
origin = ent.position
|
|
103
|
+
|
|
104
|
+
if player_pos.x >= origin.x + mins.x - player_radius &&
|
|
105
|
+
player_pos.x <= origin.x + maxs.x + player_radius &&
|
|
106
|
+
player_pos.y >= origin.y + mins.y - player_radius &&
|
|
107
|
+
player_pos.y <= origin.y + maxs.y + player_radius &&
|
|
108
|
+
player_pos.z >= origin.z + mins.z - player_radius &&
|
|
109
|
+
player_pos.z <= origin.z + maxs.z + player_radius
|
|
110
|
+
fire_targets(ent.target) if ent.target
|
|
111
|
+
yield ent if block_given?
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def init_brush_entity(ent)
|
|
119
|
+
return unless ent.model_index
|
|
120
|
+
|
|
121
|
+
model = @level.models[ent.model_index]
|
|
122
|
+
return unless model
|
|
123
|
+
|
|
124
|
+
# Store the closed (original) position
|
|
125
|
+
ent.instance_variable_set(:@closed_pos, ent.position)
|
|
126
|
+
ent.instance_variable_set(:@open_pos, ent.position)
|
|
127
|
+
ent.instance_variable_set(:@move_fraction, 0.0)
|
|
128
|
+
|
|
129
|
+
case ent.classname
|
|
130
|
+
when "func_door"
|
|
131
|
+
setup_door(ent, model)
|
|
132
|
+
when "func_plat"
|
|
133
|
+
setup_platform(ent, model)
|
|
134
|
+
when "func_button"
|
|
135
|
+
setup_button(ent, model)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
DOOR_START_OPEN = 1
|
|
140
|
+
|
|
141
|
+
def setup_door(ent, model)
|
|
142
|
+
# Calculate move direction from angle
|
|
143
|
+
dir = ent.forward_vector
|
|
144
|
+
size = model.maxs - model.mins
|
|
145
|
+
move_dist = dir.x.abs * size.x + dir.y.abs * size.y + dir.z.abs * size.z
|
|
146
|
+
move_dist -= ent.lip
|
|
147
|
+
|
|
148
|
+
pos1 = ent.position # authored = "closed"
|
|
149
|
+
pos2 = ent.position + dir * move_dist # offset = "open"
|
|
150
|
+
|
|
151
|
+
# DOOR_START_OPEN: door is authored at the open position. Swap so
|
|
152
|
+
# that pos1 = closed (down/hidden) and pos2 = open (visible).
|
|
153
|
+
spawnflags = (ent["spawnflags"] || "0").to_i
|
|
154
|
+
if (spawnflags & DOOR_START_OPEN) != 0
|
|
155
|
+
ent.position = pos2
|
|
156
|
+
ent.instance_variable_set(:@closed_pos, pos2)
|
|
157
|
+
ent.instance_variable_set(:@open_pos, pos1)
|
|
158
|
+
else
|
|
159
|
+
ent.instance_variable_set(:@closed_pos, pos1)
|
|
160
|
+
ent.instance_variable_set(:@open_pos, pos2)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def setup_platform(ent, model)
|
|
165
|
+
# In Quake, the map origin is the TOP position.
|
|
166
|
+
# See plats.qc func_plat:
|
|
167
|
+
# pos1 = top (authored origin)
|
|
168
|
+
# pos2 = top - (height - 8) // 8-unit lip stays visible
|
|
169
|
+
# Platforms WITHOUT a targetname start at the bottom and rise when
|
|
170
|
+
# the player steps on them. Platforms WITH a targetname start at
|
|
171
|
+
# the top and go down when triggered.
|
|
172
|
+
size = model.maxs - model.mins
|
|
173
|
+
height = size.z - 8.0
|
|
174
|
+
|
|
175
|
+
top_pos = ent.position
|
|
176
|
+
bottom_pos = Math::Vec3.new(top_pos.x, top_pos.y, top_pos.z - height)
|
|
177
|
+
|
|
178
|
+
if ent.targetname
|
|
179
|
+
# Targeted: start at top, button press lowers it
|
|
180
|
+
ent.instance_variable_set(:@closed_pos, top_pos)
|
|
181
|
+
ent.instance_variable_set(:@open_pos, bottom_pos)
|
|
182
|
+
else
|
|
183
|
+
# Normal: start at bottom, player steps on to ride up
|
|
184
|
+
ent.position = bottom_pos
|
|
185
|
+
ent.instance_variable_set(:@closed_pos, bottom_pos)
|
|
186
|
+
ent.instance_variable_set(:@open_pos, top_pos)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def setup_button(ent, model)
|
|
191
|
+
dir = ent.forward_vector
|
|
192
|
+
size = model.maxs - model.mins
|
|
193
|
+
move_dist = dir.x.abs * size.x + dir.y.abs * size.y + dir.z.abs * size.z
|
|
194
|
+
move_dist -= ent.lip
|
|
195
|
+
|
|
196
|
+
ent.instance_variable_set(:@open_pos, ent.position + dir * move_dist)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def update_door(ent, dt, player_pos)
|
|
200
|
+
case ent.state
|
|
201
|
+
when STATE_IDLE, STATE_CLOSED
|
|
202
|
+
# Check if player is near or if targeted
|
|
203
|
+
if near_entity?(ent, player_pos, 128.0) || ent.instance_variable_get(:@triggered)
|
|
204
|
+
ent.state = STATE_OPENING
|
|
205
|
+
ent.instance_variable_set(:@triggered, false)
|
|
206
|
+
end
|
|
207
|
+
when STATE_OPENING
|
|
208
|
+
move_toward(ent, ent.instance_variable_get(:@open_pos), dt)
|
|
209
|
+
if ent.instance_variable_get(:@move_fraction) >= 1.0
|
|
210
|
+
ent.state = STATE_OPEN
|
|
211
|
+
ent.think_time = ent.wait
|
|
212
|
+
end
|
|
213
|
+
when STATE_OPEN
|
|
214
|
+
ent.think_time -= dt
|
|
215
|
+
if ent.think_time <= 0 && ent.wait >= 0
|
|
216
|
+
ent.state = STATE_CLOSING
|
|
217
|
+
end
|
|
218
|
+
when STATE_CLOSING
|
|
219
|
+
move_toward(ent, ent.instance_variable_get(:@closed_pos), dt)
|
|
220
|
+
if ent.instance_variable_get(:@move_fraction) >= 1.0
|
|
221
|
+
ent.state = STATE_CLOSED
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def update_platform(ent, dt, player_pos)
|
|
227
|
+
case ent.state
|
|
228
|
+
when STATE_IDLE
|
|
229
|
+
model = @level.models[ent.model_index]
|
|
230
|
+
triggered = ent.instance_variable_get(:@triggered)
|
|
231
|
+
on_top = model && on_top_of?(ent, model, player_pos)
|
|
232
|
+
|
|
233
|
+
# Targeted platforms only react to button triggers.
|
|
234
|
+
# Normal platforms also activate when player steps on them.
|
|
235
|
+
should_move = if ent.targetname
|
|
236
|
+
triggered
|
|
237
|
+
else
|
|
238
|
+
triggered || on_top
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
if should_move
|
|
242
|
+
ent.state = STATE_OPENING
|
|
243
|
+
ent.instance_variable_set(:@triggered, false)
|
|
244
|
+
end
|
|
245
|
+
when STATE_OPENING
|
|
246
|
+
move_toward(ent, ent.instance_variable_get(:@open_pos), dt)
|
|
247
|
+
if ent.instance_variable_get(:@move_fraction) >= 1.0
|
|
248
|
+
ent.state = STATE_OPEN
|
|
249
|
+
ent.think_time = ent.wait
|
|
250
|
+
end
|
|
251
|
+
when STATE_OPEN
|
|
252
|
+
ent.think_time -= dt
|
|
253
|
+
if ent.think_time <= 0
|
|
254
|
+
ent.state = STATE_CLOSING
|
|
255
|
+
end
|
|
256
|
+
when STATE_CLOSING
|
|
257
|
+
move_toward(ent, ent.instance_variable_get(:@closed_pos), dt)
|
|
258
|
+
if ent.instance_variable_get(:@move_fraction) >= 1.0
|
|
259
|
+
ent.state = STATE_IDLE
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def update_button(ent, dt, player_pos)
|
|
265
|
+
case ent.state
|
|
266
|
+
when STATE_IDLE, STATE_CLOSED
|
|
267
|
+
if near_entity?(ent, player_pos, 64.0) || ent.instance_variable_get(:@triggered)
|
|
268
|
+
ent.state = STATE_OPENING
|
|
269
|
+
ent.instance_variable_set(:@triggered, false)
|
|
270
|
+
fire_targets(ent.target)
|
|
271
|
+
end
|
|
272
|
+
when STATE_OPENING
|
|
273
|
+
move_toward(ent, ent.instance_variable_get(:@open_pos), dt)
|
|
274
|
+
if ent.instance_variable_get(:@move_fraction) >= 1.0
|
|
275
|
+
ent.state = STATE_OPEN
|
|
276
|
+
ent.think_time = ent.wait
|
|
277
|
+
end
|
|
278
|
+
when STATE_OPEN
|
|
279
|
+
ent.think_time -= dt
|
|
280
|
+
if ent.think_time <= 0
|
|
281
|
+
ent.state = STATE_CLOSING
|
|
282
|
+
end
|
|
283
|
+
when STATE_CLOSING
|
|
284
|
+
move_toward(ent, ent.instance_variable_get(:@closed_pos), dt)
|
|
285
|
+
if ent.instance_variable_get(:@move_fraction) >= 1.0
|
|
286
|
+
ent.state = STATE_CLOSED
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def update_train(ent, dt)
|
|
292
|
+
# Trains follow path_corner entities - basic movement only
|
|
293
|
+
return unless ent.state == STATE_OPENING
|
|
294
|
+
|
|
295
|
+
move_toward(ent, ent.instance_variable_get(:@open_pos), dt)
|
|
296
|
+
if ent.instance_variable_get(:@move_fraction) >= 1.0
|
|
297
|
+
ent.state = STATE_IDLE
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def move_toward(ent, target, dt)
|
|
302
|
+
current = ent.position
|
|
303
|
+
diff = target - current
|
|
304
|
+
dist = diff.length
|
|
305
|
+
|
|
306
|
+
# Already at target
|
|
307
|
+
if dist < 0.1
|
|
308
|
+
ent.position = target
|
|
309
|
+
ent.instance_variable_set(:@move_fraction, 1.0)
|
|
310
|
+
return
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
move_amount = ent.speed * dt
|
|
314
|
+
if move_amount >= dist
|
|
315
|
+
# Snap to target
|
|
316
|
+
ent.position = target
|
|
317
|
+
ent.instance_variable_set(:@move_fraction, 1.0)
|
|
318
|
+
else
|
|
319
|
+
# Partial movement, not done yet
|
|
320
|
+
ent.position = current + diff * (move_amount / dist)
|
|
321
|
+
ent.instance_variable_set(:@move_fraction, 0.0)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def near_entity?(ent, player_pos, radius)
|
|
326
|
+
model = @level.models[ent.model_index]
|
|
327
|
+
return false unless model
|
|
328
|
+
|
|
329
|
+
# Check distance to model center
|
|
330
|
+
center = ent.position + (model.mins + model.maxs) * 0.5
|
|
331
|
+
dx = player_pos.x - center.x
|
|
332
|
+
dy = player_pos.y - center.y
|
|
333
|
+
dz = player_pos.z - center.z
|
|
334
|
+
(dx * dx + dy * dy + dz * dz) < radius * radius
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def on_top_of?(ent, model, player_pos)
|
|
338
|
+
# Check if player is standing on top of the platform.
|
|
339
|
+
# Tolerance: -8 to +24 units above the surface (player feet within
|
|
340
|
+
# one stair step of the platform top).
|
|
341
|
+
top_z = ent.position.z + model.maxs.z
|
|
342
|
+
player_pos.z >= top_z - 8 && player_pos.z <= top_z + 24 &&
|
|
343
|
+
player_pos.x >= ent.position.x + model.mins.x &&
|
|
344
|
+
player_pos.x <= ent.position.x + model.maxs.x &&
|
|
345
|
+
player_pos.y >= ent.position.y + model.mins.y &&
|
|
346
|
+
player_pos.y <= ent.position.y + model.maxs.y
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def fire_targets(target_name)
|
|
350
|
+
return unless target_name
|
|
351
|
+
|
|
352
|
+
targets = @target_map[target_name]
|
|
353
|
+
targets.each do |ent|
|
|
354
|
+
ent.instance_variable_set(:@triggered, true)
|
|
355
|
+
# Reset movement fraction for re-triggering
|
|
356
|
+
ent.instance_variable_set(:@move_fraction, 0.0)
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|