1 /** @file generator.cpp World map (particle) generator.
2 *
3 * @authors Copyright © 2003-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4 * @authors Copyright © 2006-2015 Daniel Swanson <danij@dengine.net>
5 * @authors Copyright © 2006-2007 Jamie Jones <jamie_jones_au@yahoo.com.au>
6 *
7 * @par License
8 * GPL: http://www.gnu.org/licenses/gpl.html
9 *
10 * <small>This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by the
12 * Free Software Foundation; either version 2 of the License, or (at your
13 * option) any later version. This program is distributed in the hope that it
14 * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
15 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
16 * Public License for more details. You should have received a copy of the GNU
17 * General Public License along with this program; if not, write to the Free
18 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
19 * 02110-1301 USA</small>
20 */
21
22 #include "de_platform.h"
23 #include "world/generator.h"
24
25 #include "world/clientserverworld.h" // validCount
26 #include "world/thinkers.h"
27 #include "client/cl_mobj.h"
28 #include "BspLeaf"
29 #include "ConvexSubspace"
30 #include "Surface"
31
32 #include "render/rend_model.h"
33 #include "render/rend_particle.h"
34
35 #include "client/clientsubsector.h"
36 #include "network/net_main.h"
37
38 #include "api_sound.h"
39
40 #include "Face"
41 #include "dd_def.h"
42 #include "clientapp.h"
43
44 #include <doomsday/console/var.h>
45 #include <de/String>
46 #include <de/Time>
47 #include <de/fixedpoint.h>
48 #include <de/memoryzone.h>
49 #include <de/timer.h>
50 #include <de/vector1.h>
51 #include <cmath>
52
53 using namespace de;
54
55 #define DOT2F(a,b) ( FIX2FLT(a[0]) * FIX2FLT(b[0]) + FIX2FLT(a[1]) * FIX2FLT(b[1]) )
56 #define VECMUL(a,scalar) ( a[0] = FixedMul(a[0], scalar), a[1] = FixedMul(a[1], scalar) )
57 #define VECADD(a,b) ( a[0] += b[0], a[1] += b[1] )
58 #define VECMULADD(a,scal,b) ( a[0] += FixedMul(scal, b[0]), a[1] += FixedMul(scal, b[1]) )
59 #define VECSUB(a,b) ( a[0] -= b[0], a[1] -= b[1] )
60 #define VECCPY(a,b) ( a[0] = b[0], a[1] = b[1] )
61
62 static float particleSpawnRate = 1; // Unmodified (cvar).
63
64 /**
65 * The offset is spherical and random.
66 * Low and High should be positive.
67 */
uncertainPosition(fixed_t * pos,fixed_t low,fixed_t high)68 static void uncertainPosition(fixed_t *pos, fixed_t low, fixed_t high)
69 {
70 if(!low)
71 {
72 // The simple, cubic algorithm.
73 for(int i = 0; i < 3; ++i)
74 {
75 pos[i] += (high * (RNG_RandByte() - RNG_RandByte())) * reciprocal255;
76 }
77 }
78 else
79 {
80 // The more complicated, spherical algorithm.
81 fixed_t off = ((high - low) * (RNG_RandByte() - RNG_RandByte())) * reciprocal255;
82 off += off < 0 ? -low : low;
83
84 fixed_t theta = RNG_RandByte() << (24 - ANGLETOFINESHIFT);
85 fixed_t phi = acos(2 * (RNG_RandByte() * reciprocal255) - 1) / PI * (ANGLE_180 >> ANGLETOFINESHIFT);
86
87 fixed_t vec[3];
88 vec[0] = FixedMul(fineCosine[theta], finesine[phi]);
89 vec[1] = FixedMul(finesine[theta], finesine[phi]);
90 vec[2] = FixedMul(fineCosine[phi], FLT2FIX(0.8333f));
91
92 for(int i = 0; i < 3; ++i)
93 {
94 pos[i] += FixedMul(vec[i], off);
95 }
96 }
97 }
98
99 namespace world {
100
map() const101 Map &Generator::map() const
102 {
103 return Thinker_Map(thinker);
104 }
105
id() const106 Generator::Id Generator::id() const
107 {
108 return _id;
109 }
110
setId(Id newId)111 void Generator::setId(Id newId)
112 {
113 DENG2_ASSERT(newId >= 1 && newId <= Map::MAX_GENERATORS); // 1-based
114 _id = newId;
115 }
116
age() const117 dint Generator::age() const
118 {
119 return _age;
120 }
121
origin() const122 Vector3d Generator::origin() const
123 {
124 if(source)
125 {
126 Vector3d origin(source->origin);
127 origin.z += -source->floorClip + FIX2FLT(originAtSpawn[2]);
128 return origin;
129 }
130
131 return Vector3d(FIX2FLT(originAtSpawn[0]), FIX2FLT(originAtSpawn[1]), FIX2FLT(originAtSpawn[2]));
132 }
133
clearParticles()134 void Generator::clearParticles()
135 {
136 Z_Free(_pinfo);
137 _pinfo = nullptr;
138 }
139
configureFromDef(ded_ptcgen_t const * newDef)140 void Generator::configureFromDef(ded_ptcgen_t const *newDef)
141 {
142 DENG2_ASSERT(newDef);
143
144 if(count <= 0)
145 count = 1;
146
147 // Make sure no generator is type-triggered by default.
148 type = type2 = -1;
149
150 def = newDef;
151 _flags = Flags(def->flags);
152 _pinfo = (ParticleInfo *) Z_Calloc(sizeof(ParticleInfo) * count, PU_MAP, 0);
153 stages = (ParticleStage *) Z_Calloc(sizeof(ParticleStage) * def->stages.size(), PU_MAP, 0);
154
155 for(dint i = 0; i < def->stages.size(); ++i)
156 {
157 ded_ptcstage_t const *sdef = &def->stages[i];
158 ParticleStage *s = &stages[i];
159
160 s->bounce = FLT2FIX(sdef->bounce);
161 s->resistance = FLT2FIX(1 - sdef->resistance);
162 s->radius = FLT2FIX(sdef->radius);
163 s->gravity = FLT2FIX(sdef->gravity);
164 s->type = sdef->type;
165 s->flags = ParticleStage::Flags(sdef->flags);
166 }
167
168 // Init some data.
169 for(dint i = 0; i < 3; ++i)
170 {
171 originAtSpawn[i] = FLT2FIX(def->center[i]);
172 vector[i] = FLT2FIX(def->vector[i]);
173 }
174
175 // Apply a random component to the spawn vector.
176 if(def->initVectorVariance > 0)
177 {
178 uncertainPosition(vector, 0, FLT2FIX(def->initVectorVariance));
179 }
180
181 // Mark unused.
182 for(dint i = 0; i < count; ++i)
183 {
184 ParticleInfo* pinfo = &_pinfo[i];
185 pinfo->stage = -1;
186 }
187 }
188
presimulate(dint tics)189 void Generator::presimulate(dint tics)
190 {
191 for(; tics > 0; tics--)
192 {
193 runTick();
194 }
195
196 // Reset age so presim doesn't affect it.
197 _age = 0;
198 }
199
isStatic() const200 bool Generator::isStatic() const
201 {
202 return _flags.testFlag(Static);
203 }
204
isUntriggered() const205 bool Generator::isUntriggered() const
206 {
207 return _untriggered;
208 }
209
setUntriggered(bool yes)210 void Generator::setUntriggered(bool yes)
211 {
212 _untriggered = yes;
213 }
214
blendmode() const215 blendmode_t Generator::blendmode() const
216 {
217 /// @todo Translate these old flags once, during definition parsing -ds
218 if(_flags.testFlag(BlendAdditive)) return BM_ADD;
219 if(_flags.testFlag(BlendSubtract)) return BM_SUBTRACT;
220 if(_flags.testFlag(BlendReverseSubtract)) return BM_REVERSE_SUBTRACT;
221 if(_flags.testFlag(BlendMultiply)) return BM_MUL;
222 if(_flags.testFlag(BlendInverseMultiply)) return BM_INVERSE_MUL;
223 return BM_NORMAL;
224 }
225
activeParticleCount() const226 dint Generator::activeParticleCount() const
227 {
228 dint numActive = 0;
229 for(dint i = 0; i < count; ++i)
230 {
231 if(_pinfo[i].stage >= 0)
232 {
233 numActive += 1;
234 }
235 }
236 return numActive;
237 }
238
particleInfo() const239 ParticleInfo const *Generator::particleInfo() const
240 {
241 return _pinfo;
242 }
243
setParticleAngles(ParticleInfo * pinfo,dint flags)244 static void setParticleAngles(ParticleInfo *pinfo, dint flags)
245 {
246 DENG2_ASSERT(pinfo);
247
248 if(flags & Generator::ParticleStage::ZeroYaw)
249 pinfo->yaw = 0;
250 if(flags & Generator::ParticleStage::ZeroPitch)
251 pinfo->pitch = 0;
252 if(flags & Generator::ParticleStage::RandomYaw)
253 pinfo->yaw = RNG_RandFloat() * 65536;
254 if(flags & Generator::ParticleStage::RandomPitch)
255 pinfo->pitch = RNG_RandFloat() * 65536;
256 }
257
particleSound(fixed_t pos[3],ded_embsound_t * sound)258 static void particleSound(fixed_t pos[3], ded_embsound_t *sound)
259 {
260 DENG2_ASSERT(pos && sound);
261
262 // Is there any sound to play?
263 if(!sound->id || sound->volume <= 0) return;
264
265 ddouble orig[3];
266 for(dint i = 0; i < 3; ++i)
267 {
268 orig[i] = FIX2FLT(pos[i]);
269 }
270
271 S_LocalSoundAtVolumeFrom(sound->id, nullptr, orig, sound->volume);
272 }
273
newParticle()274 dint Generator::newParticle()
275 {
276 #ifdef __CLIENT__
277 // Check for model-only generators.
278 dfloat inter = -1;
279 FrameModelDef *mf = 0, *nextmf = 0;
280 if(source)
281 {
282 mf = Mobj_ModelDef(*source, &nextmf, &inter);
283 if(((!mf || !useModels) && def->flags & ModelOnly) ||
284 (mf && useModels && mf->flags & MFF_NO_PARTICLES))
285 return -1;
286 }
287
288 // Keep the spawn cursor in the valid range.
289 if(++_spawnCP >= count)
290 {
291 _spawnCP -= count;
292 }
293
294 dint const newParticleIdx = _spawnCP;
295
296 // Set the particle's data.
297 ParticleInfo *pinfo = &_pinfo[_spawnCP];
298 pinfo->stage = 0;
299 if(RNG_RandFloat() < def->altStartVariance)
300 {
301 pinfo->stage = def->altStart;
302 }
303
304 pinfo->tics = def->stages[pinfo->stage].tics *
305 (1 - def->stages[pinfo->stage].variance * RNG_RandFloat());
306
307 // Launch vector.
308 pinfo->mov[0] = vector[0];
309 pinfo->mov[1] = vector[1];
310 pinfo->mov[2] = vector[2];
311
312 // Apply some random variance.
313 pinfo->mov[0] += FLT2FIX(def->vectorVariance * (RNG_RandFloat() - RNG_RandFloat()));
314 pinfo->mov[1] += FLT2FIX(def->vectorVariance * (RNG_RandFloat() - RNG_RandFloat()));
315 pinfo->mov[2] += FLT2FIX(def->vectorVariance * (RNG_RandFloat() - RNG_RandFloat()));
316
317 // Apply some aspect ratio scaling to the momentum vector.
318 // This counters the 200/240 difference nearly completely.
319 pinfo->mov[0] = FixedMul(pinfo->mov[0], FLT2FIX(1.1f));
320 pinfo->mov[1] = FixedMul(pinfo->mov[1], FLT2FIX(0.95f));
321 pinfo->mov[2] = FixedMul(pinfo->mov[2], FLT2FIX(1.1f));
322
323 // Set proper speed.
324 fixed_t uncertain = FLT2FIX(def->speed * (1 - def->speedVariance * RNG_RandFloat()));
325
326 fixed_t len = FLT2FIX(M_ApproxDistancef(
327 M_ApproxDistancef(FIX2FLT(pinfo->mov[0]), FIX2FLT(pinfo->mov[1])), FIX2FLT(pinfo->mov[2])));
328 if(!len) len = FRACUNIT;
329 len = FixedDiv(uncertain, len);
330
331 pinfo->mov[0] = FixedMul(pinfo->mov[0], len);
332 pinfo->mov[1] = FixedMul(pinfo->mov[1], len);
333 pinfo->mov[2] = FixedMul(pinfo->mov[2], len);
334
335 // The source is a mobj?
336 if(source)
337 {
338 if(_flags & RelativeVector)
339 {
340 // Rotate the vector using the source angle.
341 dfloat temp[3];
342
343 temp[0] = FIX2FLT(pinfo->mov[0]);
344 temp[1] = FIX2FLT(pinfo->mov[1]);
345 temp[2] = 0;
346
347 // Player visangles have some problems, let's not use them.
348 M_RotateVector(temp, source->angle / (float) ANG180 * -180 + 90, 0);
349
350 pinfo->mov[0] = FLT2FIX(temp[0]);
351 pinfo->mov[1] = FLT2FIX(temp[1]);
352 }
353
354 if(_flags & RelativeVelocity)
355 {
356 pinfo->mov[0] += FLT2FIX(source->mom[MX]);
357 pinfo->mov[1] += FLT2FIX(source->mom[MY]);
358 pinfo->mov[2] += FLT2FIX(source->mom[MZ]);
359 }
360
361 // Origin.
362 pinfo->origin[0] = FLT2FIX(source->origin[0]);
363 pinfo->origin[1] = FLT2FIX(source->origin[1]);
364 pinfo->origin[2] = FLT2FIX(source->origin[2] - source->floorClip);
365
366 uncertainPosition(pinfo->origin, FLT2FIX(def->spawnRadiusMin), FLT2FIX(def->spawnRadius));
367
368 // Offset to the real center.
369 pinfo->origin[2] += originAtSpawn[2];
370
371 // Include bobbing in the spawn height.
372 pinfo->origin[2] -= FLT2FIX(Mobj_BobOffset(*source));
373
374 // Calculate XY center with mobj angle.
375 angle_t const angle = Mobj_AngleSmoothed(source) + (fixed_t) (FIX2FLT(originAtSpawn[1]) / 180.0f * ANG180);
376 duint const an = angle >> ANGLETOFINESHIFT;
377 duint const an2 = (angle + ANG90) >> ANGLETOFINESHIFT;
378
379 pinfo->origin[0] += FixedMul(fineCosine[an], originAtSpawn[0]);
380 pinfo->origin[1] += FixedMul(finesine[an], originAtSpawn[0]);
381
382 // There might be an offset from the model of the mobj.
383 if(mf && (mf->testSubFlag(0, MFF_PARTICLE_SUB1) || def->subModel >= 0))
384 {
385 dfloat off[3] = { 0, 0, 0 };
386 dint subidx;
387
388 // Select the right submodel to use as the origin.
389 if(def->subModel >= 0)
390 subidx = def->subModel;
391 else
392 subidx = 1; // Default to submodel #1.
393
394 // Interpolate the offset.
395 if(inter > 0 && nextmf)
396 {
397 off[0] = nextmf->particleOffset(subidx)[0] - mf->particleOffset(subidx)[0];
398 off[1] = nextmf->particleOffset(subidx)[1] - mf->particleOffset(subidx)[1];
399 off[2] = nextmf->particleOffset(subidx)[2] - mf->particleOffset(subidx)[2];
400
401 off[0] *= inter;
402 off[1] *= inter;
403 off[2] *= inter;
404 }
405
406 off[0] += mf->particleOffset(subidx)[0];
407 off[1] += mf->particleOffset(subidx)[1];
408 off[2] += mf->particleOffset(subidx)[2];
409
410 // Apply it to the particle coords.
411 pinfo->origin[0] += FixedMul(fineCosine[an], FLT2FIX(off[0]));
412 pinfo->origin[0] += FixedMul(fineCosine[an2], FLT2FIX(off[2]));
413 pinfo->origin[1] += FixedMul(finesine[an], FLT2FIX(off[0]));
414 pinfo->origin[1] += FixedMul(finesine[an2], FLT2FIX(off[2]));
415 pinfo->origin[2] += FLT2FIX(off[1]);
416 }
417 }
418 else if(plane)
419 {
420 /// @todo fixme: ignorant of mapped sector planes.
421 fixed_t radius = stages[pinfo->stage].radius;
422 Sector const *sector = &plane->sector();
423
424 // Choose a random spot inside the sector, on the spawn plane.
425 if(_flags & SpawnSpace)
426 {
427 pinfo->origin[2] =
428 FLT2FIX(sector->floor().height()) + radius +
429 FixedMul(RNG_RandByte() << 8,
430 FLT2FIX(sector->ceiling().height() -
431 sector->floor().height()) - 2 * radius);
432 }
433 else if((_flags & SpawnFloor) ||
434 (!(_flags & (SpawnFloor | SpawnCeiling)) &&
435 plane->isSectorFloor()))
436 {
437 // Spawn on the floor.
438 pinfo->origin[2] = FLT2FIX(plane->height()) + radius;
439 }
440 else
441 {
442 // Spawn on the ceiling.
443 pinfo->origin[2] = FLT2FIX(plane->height()) - radius;
444 }
445
446 /**
447 * Choosing the XY spot is a bit more difficult.
448 * But we must be fast and only sufficiently accurate.
449 *
450 * @todo Nothing prevents spawning on the wrong side (or inside) of one-sided
451 * walls (large diagonal subspaces!).
452 */
453 ConvexSubspace *subspace = 0;
454 for(dint i = 0; i < 5; ++i) // Try a couple of times (max).
455 {
456 dfloat x = sector->bounds().minX +
457 RNG_RandFloat() * (sector->bounds().maxX - sector->bounds().minX);
458 dfloat y = sector->bounds().minY +
459 RNG_RandFloat() * (sector->bounds().maxY - sector->bounds().minY);
460
461 subspace = map().bspLeafAt(Vector2d(x, y)).subspacePtr();
462 if(subspace && sector == &subspace->sector())
463 break;
464
465 subspace = 0;
466 }
467
468 if(!subspace)
469 {
470 pinfo->stage = -1;
471 return -1;
472 }
473
474 AABoxd const &subBounds = subspace->poly().bounds();
475
476 // Try a couple of times to get a good random spot.
477 dint tries;
478 for(tries = 0; tries < 10; ++tries) // Max this many tries before giving up.
479 {
480 dfloat x = subBounds.minX +
481 RNG_RandFloat() * (subBounds.maxX - subBounds.minX);
482 dfloat y = subBounds.minY +
483 RNG_RandFloat() * (subBounds.maxY - subBounds.minY);
484
485 pinfo->origin[0] = FLT2FIX(x);
486 pinfo->origin[1] = FLT2FIX(y);
487
488 if(subspace == map().bspLeafAt(Vector2d(x, y)).subspacePtr())
489 break; // This is a good place.
490 }
491
492 if(tries == 10) // No good place found?
493 {
494 pinfo->stage = -1; // Damn.
495 return -1;
496 }
497 }
498 else if(isUntriggered())
499 {
500 // The center position is the spawn origin.
501 pinfo->origin[0] = originAtSpawn[0];
502 pinfo->origin[1] = originAtSpawn[1];
503 pinfo->origin[2] = originAtSpawn[2];
504 uncertainPosition(pinfo->origin, FLT2FIX(def->spawnRadiusMin),
505 FLT2FIX(def->spawnRadius));
506 }
507
508 // Initial angles for the particle.
509 setParticleAngles(pinfo, def->stages[pinfo->stage].flags);
510
511 // The other place where this gets updated is after moving over
512 // a two-sided line.
513 /*if(plane)
514 {
515 pinfo->sector = &plane->sector();
516 }
517 else*/
518 {
519 Vector2d ptOrigin(FIX2FLT(pinfo->origin[0]), FIX2FLT(pinfo->origin[1]));
520 pinfo->bspLeaf = &map().bspLeafAt(ptOrigin);
521
522 // A BSP leaf with no geometry is not a suitable place for a particle.
523 if(!pinfo->bspLeaf->hasSubspace())
524 {
525 pinfo->stage = -1;
526 return -1;
527 }
528 }
529
530 // Play a stage sound?
531 particleSound(pinfo->origin, &def->stages[pinfo->stage].sound);
532
533 return newParticleIdx;
534 #else // !__CLIENT__
535 return -1;
536 #endif
537 }
538
539 #ifdef __CLIENT__
540
541 /**
542 * Callback for the client mobj iterator, called from P_PtcGenThinker.
543 */
newGeneratorParticlesWorker(mobj_t * cmo,void * context)544 static dint newGeneratorParticlesWorker(mobj_t *cmo, void *context)
545 {
546 Generator *gen = (Generator *) context;
547 ClientMobjThinkerData::RemoteSync *info = ClMobj_GetInfo(cmo);
548
549 // If the clmobj is not valid at the moment, don't do anything.
550 if(info->flags & (CLMF_UNPREDICTABLE | CLMF_HIDDEN))
551 {
552 return false;
553 }
554
555 if(cmo->type != gen->type && cmo->type != gen->type2)
556 {
557 // Type mismatch.
558 return false;
559 }
560
561 gen->source = cmo;
562 gen->newParticle();
563 return false;
564 }
565
566 #endif
567
568 /**
569 * Particle touches something solid. Returns false iff the particle dies.
570 */
touchParticle(ParticleInfo * pinfo,Generator::ParticleStage * stage,ded_ptcstage_t * stageDef,bool touchWall)571 static dint touchParticle(ParticleInfo *pinfo, Generator::ParticleStage *stage,
572 ded_ptcstage_t *stageDef, bool touchWall)
573 {
574 // Play a hit sound.
575 particleSound(pinfo->origin, &stageDef->hitSound);
576
577 if(stage->flags.testFlag(Generator::ParticleStage::DieTouch))
578 {
579 // Particle dies from touch.
580 pinfo->stage = -1;
581 return false;
582 }
583
584 if(stage->flags.testFlag(Generator::ParticleStage::StageTouch) ||
585 (touchWall && stage->flags.testFlag(Generator::ParticleStage::StageWallTouch)) ||
586 (!touchWall && stage->flags.testFlag(Generator::ParticleStage::StageFlatTouch)))
587 {
588 // Particle advances to the next stage.
589 pinfo->tics = 0;
590 }
591
592 // Particle survives the touch.
593 return true;
594 }
595
particleZ(ParticleInfo const & pinfo) const596 dfloat Generator::particleZ(ParticleInfo const &pinfo) const
597 {
598 auto const &subsec = pinfo.bspLeaf->subspace().subsector().as<world::ClientSubsector>();
599 if(pinfo.origin[2] == DDMAXINT)
600 {
601 return subsec.visCeiling().heightSmoothed() - 2;
602 }
603 if(pinfo.origin[2] == DDMININT)
604 {
605 return (subsec.visFloor().heightSmoothed() + 2);
606 }
607 return FIX2FLT(pinfo.origin[2]);
608 }
609
particleOrigin(ParticleInfo const & pt) const610 Vector3f Generator::particleOrigin(ParticleInfo const &pt) const
611 {
612 return Vector3f(FIX2FLT(pt.origin[0]), FIX2FLT(pt.origin[1]), particleZ(pt));
613 }
614
particleMomentum(ParticleInfo const & pt) const615 Vector3f Generator::particleMomentum(ParticleInfo const &pt) const
616 {
617 return Vector3f(FIX2FLT(pt.mov[0]), FIX2FLT(pt.mov[1]), FIX2FLT(pt.mov[2]));
618 }
619
spinParticle(ParticleInfo & pinfo)620 void Generator::spinParticle(ParticleInfo &pinfo)
621 {
622 static dint const yawSigns[4] = { 1, 1, -1, -1 };
623 static dint const pitchSigns[4] = { 1, -1, 1, -1 };
624
625 ded_ptcstage_t const *stDef = &def->stages[pinfo.stage];
626 duint const spinIndex = uint(&pinfo - &_pinfo[id() / 8]) % 4;
627
628 DENG2_ASSERT(spinIndex < 4);
629
630 dint const yawSign = yawSigns[spinIndex];
631 dint const pitchSign = pitchSigns[spinIndex];
632
633 if(stDef->spin[0] != 0)
634 {
635 pinfo.yaw += 65536 * yawSign * stDef->spin[0] / (360 * TICSPERSEC);
636 }
637 if(stDef->spin[1] != 0)
638 {
639 pinfo.pitch += 65536 * pitchSign * stDef->spin[1] / (360 * TICSPERSEC);
640 }
641
642 pinfo.yaw *= 1 - stDef->spinResistance[0];
643 pinfo.pitch *= 1 - stDef->spinResistance[1];
644 }
645
moveParticle(dint index)646 void Generator::moveParticle(dint index)
647 {
648 DENG2_ASSERT(index >= 0 && index < count);
649
650 ParticleInfo *pinfo = &_pinfo[index];
651 ParticleStage *st = &stages[pinfo->stage];
652 ded_ptcstage_t *stDef = &def->stages[pinfo->stage];
653
654 // Particle rotates according to spin speed.
655 spinParticle(*pinfo);
656
657 // Changes to momentum.
658 /// @todo Do not assume generator is from the CURRENT map.
659 pinfo->mov[2] -= FixedMul(FLT2FIX(map().gravity()), st->gravity);
660
661 // Vector force.
662 if(stDef->vectorForce[0] != 0 || stDef->vectorForce[1] != 0 ||
663 stDef->vectorForce[2] != 0)
664 {
665 for(dint i = 0; i < 3; ++i)
666 {
667 pinfo->mov[i] += FLT2FIX(stDef->vectorForce[i]);
668 }
669 }
670
671 // Sphere force pull and turn.
672 // Only applicable to sourced or untriggered generators. For other
673 // types it's difficult to define the center coordinates.
674 if(st->flags.testFlag(ParticleStage::SphereForce) &&
675 (source || isUntriggered()))
676 {
677 dfloat delta[3];
678
679 if(source)
680 {
681 delta[0] = FIX2FLT(pinfo->origin[0]) - source->origin[0];
682 delta[1] = FIX2FLT(pinfo->origin[1]) - source->origin[1];
683 delta[2] = particleZ(*pinfo) - (source->origin[2] + FIX2FLT(originAtSpawn[2]));
684 }
685 else
686 {
687 for(dint i = 0; i < 3; ++i)
688 {
689 delta[i] = FIX2FLT(pinfo->origin[i] - originAtSpawn[i]);
690 }
691 }
692
693 // Apply the offset (to source coords).
694 for(dint i = 0; i < 3; ++i)
695 {
696 delta[i] -= def->forceOrigin[i];
697 }
698
699 // Counter the aspect ratio of old times.
700 delta[2] *= 1.2f;
701
702 dfloat dist = M_ApproxDistancef(M_ApproxDistancef(delta[0], delta[1]), delta[2]);
703 if(dist != 0)
704 {
705 // Radial force pushes the particles on the surface of a sphere.
706 if(def->force)
707 {
708 // Normalize delta vector, multiply with (dist - forceRadius),
709 // multiply with radial force strength.
710 for(dint i = 0; i < 3; ++i)
711 {
712 pinfo->mov[i] -= FLT2FIX(
713 ((delta[i] / dist) * (dist - def->forceRadius)) * def->force);
714 }
715 }
716
717 // Rotate!
718 if(def->forceAxis[0] || def->forceAxis[1] || def->forceAxis[2])
719 {
720 dfloat cross[3];
721 V3f_CrossProduct(cross, def->forceAxis, delta);
722
723 for(dint i = 0; i < 3; ++i)
724 {
725 pinfo->mov[i] += FLT2FIX(cross[i]) >> 8;
726 }
727 }
728 }
729 }
730
731 if(st->resistance != FRACUNIT)
732 {
733 for(dint i = 0; i < 3; ++i)
734 {
735 pinfo->mov[i] = FixedMul(pinfo->mov[i], st->resistance);
736 }
737 }
738
739 // The particle is 'soft': half of radius is ignored.
740 // The exception is plane flat particles, which are rendered flat
741 // against planes. They are almost entirely soft when it comes to plane
742 // collisions.
743 fixed_t hardRadius = st->radius / 2;
744 if((st->type == PTC_POINT || (st->type >= PTC_TEXTURE && st->type < PTC_TEXTURE + MAX_PTC_TEXTURES)) &&
745 st->flags.testFlag(ParticleStage::PlaneFlat))
746 {
747 hardRadius = FRACUNIT;
748 }
749
750 // Check the new Z position only if not stuck to a plane.
751 fixed_t z = pinfo->origin[2] + pinfo->mov[2];
752 bool zBounce = false, hitFloor = false;
753 if(pinfo->origin[2] != DDMININT && pinfo->origin[2] != DDMAXINT && pinfo->bspLeaf)
754 {
755 auto &subsec = pinfo->bspLeaf->subspace().subsector().as<world::ClientSubsector>();
756 if(z > FLT2FIX(subsec.visCeiling().heightSmoothed()) - hardRadius)
757 {
758 // The Z is through the roof!
759 if(subsec.visCeiling().surface().hasSkyMaskedMaterial())
760 {
761 // Special case: particle gets lost in the sky.
762 pinfo->stage = -1;
763 return;
764 }
765
766 if(!touchParticle(pinfo, st, stDef, false))
767 return;
768
769 z = FLT2FIX(subsec.visCeiling().heightSmoothed()) - hardRadius;
770 zBounce = true;
771 hitFloor = false;
772 }
773
774 // Also check the floor.
775 if(z < FLT2FIX(subsec.visFloor().heightSmoothed()) + hardRadius)
776 {
777 if(subsec.visFloor().surface().hasSkyMaskedMaterial())
778 {
779 pinfo->stage = -1;
780 return;
781 }
782
783 if(!touchParticle(pinfo, st, stDef, false))
784 return;
785
786 z = FLT2FIX(subsec.visFloor().heightSmoothed()) + hardRadius;
787 zBounce = true;
788 hitFloor = true;
789 }
790
791 if(zBounce)
792 {
793 pinfo->mov[2] = FixedMul(-pinfo->mov[2], st->bounce);
794 if(!pinfo->mov[2])
795 {
796 // The particle has stopped moving. This means its Z-movement
797 // has ceased because of the collision with a plane. Plane-flat
798 // particles will stick to the plane.
799 if((st->type == PTC_POINT || (st->type >= PTC_TEXTURE && st->type < PTC_TEXTURE + MAX_PTC_TEXTURES)) &&
800 st->flags.testFlag(ParticleStage::PlaneFlat))
801 {
802 z = hitFloor ? DDMININT : DDMAXINT;
803 }
804 }
805 }
806
807 // Move to the new Z coordinate.
808 pinfo->origin[2] = z;
809 }
810
811 // Now check the XY direction.
812 // - Check if the movement crosses any solid lines.
813 // - If it does, quit when first one contacted and apply appropriate
814 // bounce (result depends on the angle of the contacted wall).
815 fixed_t x = pinfo->origin[0] + pinfo->mov[0];
816 fixed_t y = pinfo->origin[1] + pinfo->mov[1];
817
818 struct checklineworker_params_t
819 {
820 AABoxd box;
821 fixed_t tmpz, tmprad, tmpx1, tmpx2, tmpy1, tmpy2;
822 bool tmcross;
823 Line *ptcHitLine;
824 };
825 checklineworker_params_t clParm; zap(clParm);
826 clParm.tmcross = false; // Has crossed potential sector boundary?
827
828 // XY movement can be skipped if the particle is not moving on the
829 // XY plane.
830 if(!pinfo->mov[0] && !pinfo->mov[1])
831 {
832 // If the particle is contacting a line, there is a chance that the
833 // particle should be killed (if it's moving slowly at max).
834 if(pinfo->contact)
835 {
836 Sector *front = pinfo->contact->front().sectorPtr();
837 Sector *back = pinfo->contact->back().sectorPtr();
838
839 if(front && back && abs(pinfo->mov[2]) < FRACUNIT / 2)
840 {
841 coord_t const pz = particleZ(*pinfo);
842
843 coord_t fz;
844 if(front->floor().height() > back->floor().height())
845 {
846 fz = front->floor().height();
847 }
848 else
849 {
850 fz = back->floor().height();
851 }
852
853 coord_t cz;
854 if(front->ceiling().height() < back->ceiling().height())
855 {
856 cz = front->ceiling().height();
857 }
858 else
859 {
860 cz = back->ceiling().height();
861 }
862
863 // If the particle is in the opening of a 2-sided line, it's
864 // quite likely that it shouldn't be here...
865 if(pz > fz && pz < cz)
866 {
867 // Kill the particle.
868 pinfo->stage = -1;
869 return;
870 }
871 }
872 }
873
874 // Still not moving on the XY plane...
875 goto quit_iteration;
876 }
877
878 // We're moving in XY, so if we don't hit anything there can't be any line contact.
879 pinfo->contact = 0;
880
881 // Bounding box of the movement line.
882 clParm.tmpz = z;
883 clParm.tmprad = hardRadius;
884 clParm.tmpx1 = pinfo->origin[0];
885 clParm.tmpx2 = x;
886 clParm.tmpy1 = pinfo->origin[1];
887 clParm.tmpy2 = y;
888
889 vec2d_t point;
890 V2d_Set(point, FIX2FLT(MIN_OF(x, pinfo->origin[0]) - st->radius),
891 FIX2FLT(MIN_OF(y, pinfo->origin[1]) - st->radius));
892 V2d_InitBox(clParm.box.arvec2, point);
893 V2d_Set(point, FIX2FLT(MAX_OF(x, pinfo->origin[0]) + st->radius),
894 FIX2FLT(MAX_OF(y, pinfo->origin[1]) + st->radius));
895 V2d_AddToBox(clParm.box.arvec2, point);
896
897 // Iterate the lines in the contacted blocks.
898
899 validCount++;
900 DENG2_ASSERT(!clParm.ptcHitLine);
901 map().forAllLinesInBox(clParm.box, [&clParm] (Line &line)
902 {
903 // Does the bounding box miss the line completely?
904 if(clParm.box.maxX <= line.bounds().minX || clParm.box.minX >= line.bounds().maxX ||
905 clParm.box.maxY <= line.bounds().minY || clParm.box.minY >= line.bounds().maxY)
906 {
907 return LoopContinue;
908 }
909
910 // Movement must cross the line.
911 if((line.pointOnSide(Vector2d(FIX2FLT(clParm.tmpx1), FIX2FLT(clParm.tmpy1))) < 0) ==
912 (line.pointOnSide(Vector2d(FIX2FLT(clParm.tmpx2), FIX2FLT(clParm.tmpy2))) < 0))
913 {
914 return LoopContinue;
915 }
916
917 /*
918 * We are possibly hitting something here.
919 */
920
921 // Bounce if we hit a solid wall.
922 /// @todo fixme: What about "one-way" window lines?
923 clParm.ptcHitLine = &line;
924 if(!line.back().hasSector())
925 {
926 return LoopAbort; // Boing!
927 }
928
929 Sector *front = line.front().sectorPtr();
930 Sector *back = line.back().sectorPtr();
931
932 // Determine the opening we have here.
933 /// @todo Use R_OpenRange()
934 fixed_t ceil;
935 if(front->ceiling().height() < back->ceiling().height())
936 {
937 ceil = FLT2FIX(front->ceiling().height());
938 }
939 else
940 {
941 ceil = FLT2FIX(back->ceiling().height());
942 }
943
944 fixed_t floor;
945 if(front->floor().height() > back->floor().height())
946 {
947 floor = FLT2FIX(front->floor().height());
948 }
949 else
950 {
951 floor = FLT2FIX(back->floor().height());
952 }
953
954 // There is a backsector. We possibly might hit something.
955 if(clParm.tmpz - clParm.tmprad < floor || clParm.tmpz + clParm.tmprad > ceil)
956 {
957 return LoopAbort; // Boing!
958 }
959
960 // False alarm, continue checking.
961 clParm.ptcHitLine = nullptr;
962 // There is a possibility that the new position is in a new sector.
963 clParm.tmcross = true; // Afterwards, update the sector pointer.
964 return LoopContinue;
965 });
966
967 if(clParm.ptcHitLine)
968 {
969 fixed_t normal[2], dotp;
970
971 // Must survive the touch.
972 if(!touchParticle(pinfo, st, stDef, true))
973 return;
974
975 // There was a hit! Calculate bounce vector.
976 // - Project movement vector on the normal of hitline.
977 // - Calculate the difference to the point on the normal.
978 // - Add the difference to movement vector, negate movement.
979 // - Multiply with bounce.
980
981 // Calculate the normal.
982 normal[0] = -FLT2FIX(clParm.ptcHitLine->direction().x);
983 normal[1] = -FLT2FIX(clParm.ptcHitLine->direction().y);
984
985 if(!normal[0] && !normal[1])
986 goto quit_iteration;
987
988 // Calculate as floating point so we don't overflow.
989 dotp = FRACUNIT * (DOT2F(pinfo->mov, normal) / DOT2F(normal, normal));
990 VECMUL(normal, dotp);
991 VECSUB(normal, pinfo->mov);
992 VECMULADD(pinfo->mov, 2 * FRACUNIT, normal);
993 VECMUL(pinfo->mov, st->bounce);
994
995 // Continue from the old position.
996 x = pinfo->origin[0];
997 y = pinfo->origin[1];
998 clParm.tmcross = false; // Sector can't change if XY doesn't.
999
1000 // This line is the latest contacted line.
1001 pinfo->contact = clParm.ptcHitLine;
1002 goto quit_iteration;
1003 }
1004
1005 quit_iteration:
1006 // The move is now OK.
1007 pinfo->origin[0] = x;
1008 pinfo->origin[1] = y;
1009
1010 // Should we update the sector pointer?
1011 if(clParm.tmcross)
1012 {
1013 pinfo->bspLeaf = &map().bspLeafAt(Vector2d(FIX2FLT(x), FIX2FLT(y)));
1014
1015 // A BSP leaf with no geometry is not a suitable place for a particle.
1016 if(!pinfo->bspLeaf->hasSubspace())
1017 {
1018 // Kill the particle.
1019 pinfo->stage = -1;
1020 }
1021 }
1022 }
1023
runTick()1024 void Generator::runTick()
1025 {
1026 // Source has been destroyed?
1027 if(!isUntriggered() && !map().thinkers().isUsedMobjId(srcid))
1028 {
1029 // Blasted... Spawning new particles becomes impossible.
1030 source = nullptr;
1031 }
1032
1033 // Time to die?
1034 DENG2_ASSERT(def);
1035 if(++_age > def->maxAge && def->maxAge >= 0)
1036 {
1037 Generator_Delete(this);
1038 return;
1039 }
1040
1041 // Spawn new particles?
1042 dfloat newParts = 0;
1043 if((_age <= def->spawnAge || def->spawnAge < 0) &&
1044 (source || plane || type >= 0 || type == DED_PTCGEN_ANY_MOBJ_TYPE ||
1045 isUntriggered()))
1046 {
1047 newParts = def->spawnRate * spawnRateMultiplier;
1048
1049 newParts *= particleSpawnRate *
1050 (1 - def->spawnRateVariance * RNG_RandFloat());
1051
1052 newParts = de::min(newParts, float(def->particles)); // don't spawn too many
1053
1054 _spawnCount += newParts;
1055 while(_spawnCount >= 1)
1056 {
1057 // Spawn a new particle.
1058 if(type == DED_PTCGEN_ANY_MOBJ_TYPE || type >= 0) // Type-triggered?
1059 {
1060 #ifdef __CLIENT__
1061 // Client's should also check the client mobjs.
1062 if(isClient)
1063 {
1064 map().clMobjIterator(newGeneratorParticlesWorker, this);
1065 }
1066 #endif
1067
1068 // Spawn new particles using all applicable sources.
1069 map().thinkers().forAll(reinterpret_cast<thinkfunc_t>(gx.MobjThinker), 0x1 /*public*/, [this] (thinker_t *th)
1070 {
1071 // Type match?
1072 auto &mob = *reinterpret_cast<mobj_t *>(th);
1073 if(type == DED_PTCGEN_ANY_MOBJ_TYPE || mob.type == type || mob.type == type2)
1074 {
1075 // Someone might think this is a slight hack...
1076 source = &mob;
1077 newParticle();
1078 }
1079 return LoopContinue;
1080 });
1081
1082 // The generator has no real source.
1083 source = nullptr;
1084 }
1085 else
1086 {
1087 newParticle();
1088 }
1089
1090 _spawnCount--;
1091 }
1092 }
1093
1094 // Move particles.
1095 ParticleInfo *pinfo = _pinfo;
1096 for(dint i = 0; i < count; ++i, pinfo++)
1097 {
1098 if(pinfo->stage < 0) continue; // Not in use.
1099
1100 if(pinfo->tics-- <= 0)
1101 {
1102 // Advance to next stage.
1103 if(++pinfo->stage == def->stages.size() ||
1104 stages[pinfo->stage].type == PTC_NONE)
1105 {
1106 // Kill the particle.
1107 pinfo->stage = -1;
1108 continue;
1109 }
1110
1111 pinfo->tics = def->stages[pinfo->stage].tics * (1 - def->stages[pinfo->stage].variance * RNG_RandFloat());
1112
1113 // Change in particle angles?
1114 setParticleAngles(pinfo, def->stages[pinfo->stage].flags);
1115
1116 // Play a sound?
1117 particleSound(pinfo->origin, &def->stages[pinfo->stage].sound);
1118 }
1119
1120 // Try to move.
1121 moveParticle(i);
1122 }
1123 }
1124
consoleRegister()1125 void Generator::consoleRegister() //static
1126 {
1127 C_VAR_FLOAT("rend-particle-rate", &particleSpawnRate, 0, 0, 5);
1128 }
1129
Generator_Delete(Generator * gen)1130 void Generator_Delete(Generator *gen)
1131 {
1132 if(!gen) return;
1133 gen->map().unlink(*gen);
1134 gen->map().thinkers().remove(gen->thinker);
1135 gen->clearParticles();
1136 Z_Free(gen->stages); gen->stages = nullptr;
1137 // The generator itself is free'd when it's next turn for thinking comes.
1138 }
1139
Generator_Thinker(Generator * gen)1140 void Generator_Thinker(Generator *gen)
1141 {
1142 DENG2_ASSERT(gen != 0);
1143 gen->runTick();
1144 }
1145
1146 } // namespace world
1147