1 /* GemRB - Infinity Engine Emulator
2 * Copyright (C) 2006 The GemRB Project
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 *
18 *
19 */
20
21 #include "Projectile.h"
22
23 #include "AnimationFactory.h"
24 #include "DisplayMessage.h"
25 #include "Game.h"
26 #include "GameData.h"
27 #include "GlobalTimer.h"
28 #include "Image.h"
29 #include "Interface.h"
30 #include "ProjectileServer.h"
31 #include "Sprite2D.h"
32 #include "VEFObject.h"
33 #include "RNG.h"
34 #include "Scriptable/Actor.h"
35 #include "ScriptedAnimation.h"
36
37 #include <cmath>
38 #include <cstdlib>
39
40 namespace GemRB {
41
42 constexpr uint8_t PALSIZE = 32;
43
44 static const ieByte SixteenToNine[MAX_ORIENT]={0,1,2,3,4,5,6,7,8,7,6,5,4,3,2,1};
45 static const ieByte SixteenToFive[MAX_ORIENT]={0,1,2,3,4,3,2,1,0,1,2,3,4,3,2,1};
46
47 static ProjectileServer *server = NULL;
48
Projectile()49 Projectile::Projectile()
50 {
51 autofree = false;
52 Extension = NULL;
53 area = NULL;
54 palette = NULL;
55 Pos.empty();
56 Destination = Pos;
57 Orientation = 0;
58 NewOrientation = 0;
59 path = NULL;
60 step = NULL;
61 timeStartStep = 0;
62 phase = P_UNINITED;
63 effects = NULL;
64 children = NULL;
65 child_size = 0;
66 memset(travel, 0, sizeof(travel)) ;
67 memset(shadow, 0, sizeof(shadow)) ;
68 memset(PaletteRes,0,sizeof(PaletteRes));
69 memset(smokebam, 0, sizeof(smokebam));
70 light = NULL;
71 pathcounter = 0x7fff;
72 FakeTarget = 0;
73 bend = 0;
74 drawSpark = 0;
75 ZPos = 0;
76 extension_delay = 0;
77 Range = 0;
78 ColorSpeed = Shake = TFlags = Seq1 = Seq2 = ExtFlags = 0;
79 IDSType = IDSValue = IDSType2 = IDSValue2 = StrRef = 0;
80 LightX = LightY = LightZ = Aim = type = SparkColor = 0;
81 SmokeSpeed = SmokeAnimID = Caster = Target = Level = 0;
82 extension_explosioncount = extension_targetcount = 0;
83 Speed = 20;
84 SFlags = PSF_FLYING;
85 if (!server)
86 server = core->GetProjectileServer();
87 }
88
~Projectile()89 Projectile::~Projectile()
90 {
91 int i;
92
93 if (autofree) {
94 free(Extension);
95 }
96 delete effects;
97
98 ClearPath();
99
100 if (travel_handle) {
101 //allow an explosion sound to finish completely
102 travel_handle->StopLooping();
103 }
104
105 if (phase != P_UNINITED) {
106 for (i = 0; i < MAX_ORIENT; ++i) {
107 if(travel[i])
108 delete travel[i];
109 if(shadow[i])
110 delete shadow[i];
111 }
112 }
113
114 if(children) {
115 for(i=0;i<child_size;i++) {
116 delete children[i];
117 }
118 free (children);
119 }
120 }
121
InitExtension()122 void Projectile::InitExtension()
123 {
124 autofree = false;
125 if (!Extension) {
126 Extension = (ProjectileExtension *) calloc( 1, sizeof(ProjectileExtension));
127 }
128 }
129
CreateAnimations(Animation ** anims,const ieResRef bamres,int Seq)130 void Projectile::CreateAnimations(Animation **anims, const ieResRef bamres, int Seq)
131 {
132 AnimationFactory* af = ( AnimationFactory* )
133 gamedata->GetFactoryResource( bamres,
134 IE_BAM_CLASS_ID, IE_NORMAL );
135
136 if (!af) {
137 return;
138 }
139
140 int Max = af->GetCycleCount();
141 if (!Max) {
142 return;
143 }
144
145 if((ExtFlags&PEF_CYCLE) && !Seq) {
146 Seq=RAND(0, Max-1);
147 }
148
149 //this hack is needed because bioware .pro files are sometimes
150 //reporting bigger face count than possible by the animation
151 if (Aim>Max) Aim=Max;
152
153 if(ExtFlags&PEF_PILLAR) {
154 CreateCompositeAnimation(anims, af, Seq);
155 } else {
156 CreateOrientedAnimations(anims, af, Seq);
157 }
158 }
159
160 //Seq is the first cycle to use in the composite
161 //Aim is the number of cycles
CreateCompositeAnimation(Animation ** anims,AnimationFactory * af,int Seq)162 void Projectile::CreateCompositeAnimation(Animation **anims, AnimationFactory *af, int Seq)
163 {
164 for (int Cycle = 0; Cycle<Aim; Cycle++) {
165 int c = Cycle+Seq;
166 Animation* a = af->GetCycle( c );
167 anims[Cycle] = a;
168 if (!a) continue;
169 //animations are started at a random frame position
170 //Always start from 0, unless set otherwise
171 if (!(ExtFlags&PEF_RANDOM)) {
172 a->SetPos(0);
173 }
174
175 a->gameAnimation = true;
176 }
177 }
178
179 //Seq is the cycle to use in case of single orientations
180 //Aim is the number of Orientations
181 // FIXME: seems inefficient that we load up MAX_ORIENT animations even for those with a single orientation (default case)
CreateOrientedAnimations(Animation ** anims,AnimationFactory * af,int Seq)182 void Projectile::CreateOrientedAnimations(Animation **anims, AnimationFactory *af, int Seq)
183 {
184 for (int Cycle = 0; Cycle<MAX_ORIENT; Cycle++) {
185 bool mirror = false, mirrorvert = false;
186 int c;
187 switch(Aim) {
188 default:
189 c = Seq;
190 break;
191 case 5:
192 c = SixteenToFive[Cycle];
193 // orientations go counter-clockwise, starting south
194 if (Cycle <= 4) {
195 // bottom-right quadrant
196 mirror = false; mirrorvert = false;
197 } else if (Cycle <= 8) {
198 // top-right quadrant
199 mirror = false; mirrorvert = true;
200 } else if (Cycle < 12) {
201 // top-left quadrant
202 mirror = true; mirrorvert = true;
203 } else {
204 // bottom-left quadrant
205 mirror = true; mirrorvert = false;
206 }
207 break;
208 case 9:
209 c = SixteenToNine[Cycle];
210 if (Cycle>8) mirror=true;
211 break;
212 case 16:
213 c=Cycle;
214 break;
215 }
216 Animation* a = af->GetCycle( c );
217 anims[Cycle] = a;
218 if (!a) continue;
219 //animations are started at a random frame position
220 //Always start from 0, unless set otherwise
221 if (!(ExtFlags&PEF_RANDOM)) {
222 a->SetPos(0);
223 }
224
225 if (mirror) {
226 a->MirrorAnimation();
227 }
228 if (mirrorvert) {
229 a->MirrorAnimationVert();
230 }
231 a->gameAnimation = true;
232 }
233 }
234
235 //apply gradient colors
SetupPalette(Animation * anim[],PaletteHolder & pal,const ieByte * gradients)236 void Projectile::SetupPalette(Animation *anim[], PaletteHolder &pal, const ieByte *gradients)
237 {
238 ieDword Colors[7];
239
240 for (int i=0;i<7;i++) {
241 Colors[i]=gradients[i];
242 }
243 GetPaletteCopy(anim, pal);
244 if (pal) {
245 pal->SetupPaperdollColours(Colors, 0);
246 }
247 }
248
GetPaletteCopy(Animation * anim[],PaletteHolder & pal)249 void Projectile::GetPaletteCopy(Animation *anim[], PaletteHolder &pal)
250 {
251 if (pal)
252 return;
253 for (unsigned int i=0;i<MAX_ORIENT;i++) {
254 if (anim[i]) {
255 Holder<Sprite2D> spr = anim[i]->GetFrame(0);
256 if (spr) {
257 pal = spr->GetPalette()->Copy();
258 break;
259 }
260 }
261 }
262 }
263
SetBlend(int brighten)264 void Projectile::SetBlend(int brighten)
265 {
266 GetPaletteCopy(travel, palette);
267 if (!palette)
268 return;
269 if (!palette->HasAlpha()) {
270 palette->CreateShadedAlphaChannel();
271 }
272 if (brighten) {
273 palette->Brighten();
274 }
275 }
276
277 //create another projectile with type-1 (iterate magic missiles and call lightning)
CreateIteration()278 void Projectile::CreateIteration()
279 {
280 Projectile *pro = server->GetProjectileByIndex(type-1);
281 pro->SetEffectsCopy(effects, Pos);
282 pro->SetCaster(Caster, Level);
283 if (ExtFlags&PEF_CURVE) {
284 pro->bend=bend+1;
285 pro->Speed = Speed; // fix the different speed of MAGICMIS.pro compared to SPMAGMIS.pro
286 }
287
288 if (FakeTarget) {
289 area->AddProjectile(pro, Pos, FakeTarget, true);
290 } else {
291 area->AddProjectile(pro, Pos, Target, false);
292 }
293
294 // added by fuzzie, to make magic missiles instant, maybe wrong place
295 pro->Setup();
296 }
297
GetSmokeAnim()298 void Projectile::GetSmokeAnim()
299 {
300 int AvatarsRowNum=CharAnimations::GetAvatarsCount();
301
302 SmokeAnimID&=0xfff0; //this is a hack, i'm too lazy to figure out the subtypes
303
304 for(int i=0;i<AvatarsRowNum;i++) {
305 AvatarStruct *as = CharAnimations::GetAvatarStruct(i);
306 if (as->AnimID==SmokeAnimID) {
307 memcpy(smokebam, as->Prefixes, sizeof(ieResRef) );
308 return;
309 }
310 }
311 //turn off smoke animation if its animation was not found
312 //you might want to issue some warning here
313 TFlags&=PTF_SMOKE;
314 }
315 // load animations, start sound
Setup()316 void Projectile::Setup()
317 {
318 tint.r=128;
319 tint.g=128;
320 tint.b=128;
321 tint.a=255;
322
323 ieDword time = core->GetGame()->Ticks;
324 timeStartStep = time;
325
326 if(ExtFlags&PEF_TEXT) {
327 Actor *act = area->GetActorByGlobalID(Caster);
328 if(act) {
329 displaymsg->DisplayStringName(StrRef, DMC_LIGHTGREY, act,0);
330 }
331 }
332
333 //falling = vertical
334 //incoming = right side
335 //both = left side
336 if(ExtFlags&(PEF_FALLING|PEF_INCOMING) ) {
337 if (ExtFlags&PEF_INCOMING) {
338 if (ExtFlags&PEF_FALLING) {
339 Pos.x=Destination.x-200;
340 } else {
341 Pos.x=Destination.x+200;
342 }
343 }
344 else {
345 Pos.x=Destination.x;
346 }
347 Pos.y=Destination.y-200;
348 NextTarget(Destination);
349 }
350
351 if(ExtFlags&PEF_WALL) {
352 SetupWall();
353 }
354
355 //cone area of effect always disables the travel flag
356 //but also makes the caster immune to the effect
357 if (Extension) {
358 if (Extension->AFlags&PAF_CONE) {
359 NewOrientation = Orientation = GetOrient(Destination, Pos);
360 Destination=Pos;
361 ExtFlags|=PEF_NO_TRAVEL;
362 }
363
364 //this flag says the first explosion is delayed
365 //(works for delaying triggers too)
366 //getting the explosion count here, so an absent caster won't cut short
367 //on the explosion count
368 if(Extension->AFlags&PAF_DELAY) {
369 extension_delay=Extension->Delay;
370 } else {
371 extension_delay=0;
372 }
373 extension_explosioncount=CalculateExplosionCount();
374 }
375
376 //set any static tint
377 if(ExtFlags&PEF_TINT) {
378 uint8_t idx = PALSIZE/2;
379 const auto& pal32 = core->GetPalette32(Gradients[0]);
380 const Color& tmpColor = pal32[idx];
381 // PALSIZE is usually 12, but pst has it at 32, which is now the default, so make sure we're not trying to read an empty (black) entry
382
383 if (tmpColor.r + tmpColor.g + tmpColor.b == 0) idx = 6;
384 StaticTint(pal32[idx]);
385 }
386
387 CreateAnimations(travel, BAMRes1, Seq1);
388
389 if (TFlags&PTF_SHADOW) {
390 CreateAnimations(shadow, BAMRes2, Seq2);
391 }
392
393 if (TFlags&PTF_SMOKE) {
394 GetSmokeAnim();
395 }
396
397 //there is no travel phase, create the projectile right at the target
398 if (ExtFlags&PEF_NO_TRAVEL) {
399 Pos = Destination;
400
401 //the travel projectile should linger after explosion
402 if(ExtFlags&PEF_POP) {
403 //the explosion consists of a pop in/hold/pop out of the travel projectile (dimension door)
404 if(travel[0] && shadow[0]) {
405 extension_delay = travel[0]->GetFrameCount()*2+shadow[0]->GetFrameCount();
406 //SetDelay( travel[0]->GetFrameCount()*2+shadow[0]->GetFrameCount());
407 travel[0]->Flags|=A_ANI_PLAYONCE;
408 shadow[0]->Flags|=A_ANI_PLAYONCE;
409 }
410 } else {
411 if(travel[0]) {
412 extension_delay = travel[0]->GetFrameCount();
413 travel[0]->Flags|=A_ANI_PLAYONCE;
414 //SetDelay(travel[0]->GetFrameCount() );
415 }
416 }
417 }
418
419 if (TFlags&PTF_COLOUR) {
420 SetupPalette(travel, palette, Gradients);
421 } else {
422 palette = gamedata->GetPalette(PaletteRes);
423 }
424
425 if (TFlags&PTF_LIGHT) {
426 light = core->GetVideoDriver()->CreateLight(LightX, LightZ);
427 }
428 if (TFlags&PTF_TRANS) {
429 SetBlend(TFlags&PTF_BRIGHTEN);
430 }
431 if (SFlags&PSF_FLYING) {
432 ZPos = FLY_HEIGHT;
433 }
434 phase = P_TRAVEL;
435 travel_handle = core->GetAudioDrv()->Play(FiringSound, SFX_CHAN_MISSILE,
436 Pos.x, Pos.y, (SFlags & PSF_LOOPING ? GEM_SND_LOOPING : 0));
437
438 //create more projectiles
439 if(ExtFlags&PEF_ITERATION) {
440 CreateIteration();
441 }
442 }
443
GetTarget()444 Actor *Projectile::GetTarget()
445 {
446 Actor *target;
447
448 if (Target) {
449 target = area->GetActorByGlobalID(Target);
450 if (!target) return NULL;
451 Actor *original = area->GetActorByGlobalID(Caster);
452 if (!effects) {
453 return target;
454 }
455 if (original == target && !effects->HasHostileEffects()) {
456 effects->SetOwner(target);
457 return target;
458 }
459
460 int res = effects->CheckImmunity ( target );
461 //resisted
462 if (!res) {
463 return NULL;
464 }
465 if (res==-1) {
466 if (original) {
467 Target = original->GetGlobalID();
468 target = original;
469 } else {
470 Log(DEBUG, "Projectile", "GetTarget: caster not found, bailing out!");
471 return NULL;
472 }
473 }
474 effects->SetOwner(original);
475 return target;
476 } else {
477 Log(DEBUG, "Projectile", "GetTarget: Target not set or dummy, using caster!");
478 }
479 target = area->GetActorByGlobalID(Caster);
480 if (target) {
481 effects->SetOwner(target);
482 }
483 return target;
484 }
485
SetDelay(int delay)486 void Projectile::SetDelay(int delay)
487 {
488 extension_delay=delay;
489 ExtFlags|=PEF_FREEZE;
490 }
491
492 //copied from Actor.cpp
493 #define ATTACKROLL 20
494 #define WEAPON_FIST 0
495 #define WEAPON_BYPASS 0x10000
496
FailedIDS(const Actor * target) const497 bool Projectile::FailedIDS(const Actor *target) const
498 {
499 bool fail = !EffectQueue::match_ids( target, IDSType, IDSValue);
500 if (ExtFlags&PEF_NOTIDS) {
501 fail = !fail;
502 }
503 if (ExtFlags&PEF_BOTH) {
504 if (!fail) {
505 fail = !EffectQueue::match_ids( target, IDSType2, IDSValue2);
506 if (ExtFlags&PEF_NOTIDS2) {
507 fail = !fail;
508 }
509 }
510 }
511 else
512 {
513 if (fail && IDSType2) {
514 fail = !EffectQueue::match_ids( target, IDSType2, IDSValue2);
515 if (ExtFlags&PEF_NOTIDS2) {
516 fail = !fail;
517 }
518 }
519 }
520
521 if (!fail) {
522 if(ExtFlags&PEF_TOUCH) {
523 Actor *caster = core->GetGame()->GetActorByGlobalID(Caster);
524 if (caster) {
525 //TODO move this to Actor
526 //TODO some projectiles use melee attack (fist), others use projectile attack
527 //this apparently depends on the spell's SpellForm (normal vs. projectile)
528 int roll = caster->LuckyRoll(1, ATTACKROLL, 0);
529 if (roll==1) {
530 return true; //critical failure
531 }
532
533 if (!(target->GetStat(IE_STATE_ID)&STATE_CRIT_PROT)) {
534 if (roll >= (ATTACKROLL - (int) caster->GetStat(IE_CRITICALHITBONUS))) {
535 return false; //critical success
536 }
537 }
538
539 //handle attack type here, weapon depends on it too?
540 int tohit = caster->GetToHit(WEAPON_FIST, target);
541 //damage type, should be generic?
542 // ignore the armor bonus
543 int defense = target->GetDefense(0, WEAPON_BYPASS, caster);
544 if(target->IsReverseToHit()) {
545 fail = roll + defense < tohit;
546 } else {
547 fail = tohit + roll < defense;
548 }
549 }
550 }
551 }
552
553 return fail;
554 }
555
Payload()556 void Projectile::Payload()
557 {
558 if(Shake) {
559 core->timer.SetScreenShake(Point(Shake, Shake), Shake);
560 Shake = 0;
561 }
562
563 //allow area affecting projectile with a spell
564 if(!(effects || SuccSpell[0] || (!Target && FailSpell[0]))) {
565 return;
566 }
567
568 // PEF_CONTINUE never has a Target (LightningBolt)
569 // if we were to try to get one we would end up damaging
570 // either the caster, or the original target of the spell
571 // which are probably both nowhere near the projectile at this point
572 // all effects are applied as the projectile travels
573 if (ExtFlags & PEF_CONTINUE) {
574 delete effects;
575 effects = NULL;
576 return;
577 }
578
579 Actor *target;
580 Scriptable *Owner;
581
582 if (Target) {
583 target = GetTarget();
584 } else {
585 //the target will be the original caster
586 //in case of single point area target (dimension door)
587 if (FakeTarget) {
588 target = area->GetActorByGlobalID(FakeTarget);
589 if (!target) {
590 target = core->GetGame()->GetActorByGlobalID(FakeTarget);
591 }
592 } else {
593 target = area->GetActorByGlobalID(Caster);
594 }
595 }
596
597 if (target) {
598 Owner = area->GetScriptableByGlobalID(Caster);
599 if (!Owner) {
600 Log(WARNING, "Projectile", "Payload: Caster not found, using target!");
601 Owner = target;
602 }
603 //apply this spell on target when the projectile fails
604 if (FailedIDS(target)) {
605 if (FailSpell[0]) {
606 if (Target) {
607 core->ApplySpell(FailSpell, target, Owner, Level);
608 } else {
609 //no Target, using the fake target as owner
610 core->ApplySpellPoint(FailSpell, area, Destination, target, Level);
611 }
612 }
613 } else {
614 //apply this spell on the target when the projectile succeeds
615 if (SuccSpell[0]) {
616 core->ApplySpell(SuccSpell, target, Owner, Level);
617 }
618
619 if(ExtFlags&PEF_RGB) {
620 target->SetColorMod(0xff, RGBModifier::ADD, ColorSpeed, RGB);
621 }
622
623 if (effects) {
624 effects->SetOwner(Owner);
625 effects->AddAllEffects(target, Destination);
626 }
627 }
628 }
629
630 delete effects;
631 effects = NULL;
632 }
633
ApplyDefault()634 void Projectile::ApplyDefault()
635 {
636 Actor *actor = area->GetActorByGlobalID(Caster);
637 if (actor) {
638 //name is the projectile's name
639 //for simplicity, we apply a spell of the same name
640 core->ApplySpell(name, actor, actor, Level);
641 }
642 }
643
StopSound()644 void Projectile::StopSound()
645 {
646 if (travel_handle) {
647 travel_handle->Stop();
648 travel_handle.release();
649 }
650 }
651
UpdateSound()652 void Projectile::UpdateSound()
653 {
654 if (!(SFlags&PSF_SOUND2)) {
655 StopSound();
656 }
657 if (!travel_handle || !travel_handle->Playing()) {
658 travel_handle = core->GetAudioDrv()->Play(ArrivalSound, SFX_CHAN_MISSILE,
659 Pos.x, Pos.y, (SFlags & PSF_LOOPING2 ? GEM_SND_LOOPING : 0));
660 SFlags|=PSF_SOUND2;
661 }
662 }
663
664 //control the phase change when the projectile reached its target
665 //possible actions: vanish, hover over point, explode
666 //depends on the area extension
667 //play explosion sound
ChangePhase()668 void Projectile::ChangePhase()
669 {
670 if (Target) {
671 Actor *target = area->GetActorByGlobalID(Target);
672 if (!target) {
673 phase = P_EXPIRED;
674 return;
675 }
676 }
677
678 if (phase == P_TRAVEL) {
679 if ((ExtFlags&PEF_DELAY) && extension_delay) {
680 extension_delay--;
681 UpdateSound();
682 return;
683 }
684 }
685
686 //reached target, and explodes now
687 if (!Extension) {
688 //there are no-effect projectiles, like missed arrows
689 //Payload can redirect the projectile in case of projectile reflection
690 if (phase == P_TRAVEL) {
691 if(ExtFlags&PEF_DEFSPELL) {
692 ApplyDefault();
693 }
694 StopSound();
695 Payload();
696 phase = P_TRAVEL2;
697 }
698 //freeze on target, this is recommended only for child projectiles
699 //as the projectile won't go away on its own
700 if(ExtFlags&PEF_FREEZE) {
701 if(extension_delay) {
702 if (extension_delay>0) {
703 extension_delay--;
704 UpdateSound();
705 }
706 return;
707 }
708 }
709
710 if (phase == P_TRAVEL2) {
711 if (extension_delay) {
712 extension_delay--;
713 return;
714 }
715 }
716
717 if(ExtFlags&PEF_FADE) {
718 TFlags &= ~PTF_TINT; //turn off area tint
719 tint.a--;
720 if(tint.a>0) {
721 return;
722 }
723 }
724 }
725
726 EndTravel();
727 }
728
729 //Call this only if Extension exists!
CalculateExplosionCount()730 int Projectile::CalculateExplosionCount()
731 {
732 int count = 0;
733 Actor *act = area->GetActorByGlobalID(Caster);
734 if(act) {
735 if (Extension->AFlags&PAF_LEV_MAGE) {
736 count = act->GetMageLevel();
737 }
738 else if (Extension->AFlags&PAF_LEV_CLERIC) {
739 count = act->GetClericLevel();
740 }
741 }
742
743 if (!count) {
744 count = std::max<int>(1, Extension->ExplosionCount);
745 }
746 return count;
747 }
748
EndTravel()749 void Projectile::EndTravel()
750 {
751 StopSound();
752 UpdateSound();
753 if(!Extension) {
754 phase = P_EXPIRED;
755 return;
756 }
757
758 //this flag says that the explosion should occur only when triggered
759 if (Extension->AFlags&PAF_TRIGGER) {
760 phase = P_TRIGGER;
761 return;
762 } else {
763 phase = P_EXPLODING1;
764 }
765 }
766
767 //Note: trails couldn't be higher than VVC, but this shouldn't be a problem
AddTrail(const ieResRef BAM,const ieByte * pal) const768 int Projectile::AddTrail(const ieResRef BAM, const ieByte *pal) const
769 {
770 /*
771 VEFObject *vef=gamedata->GetVEFObject(BAM,0);
772 if (!vef) return 0;
773 ScriptedAnimation *sca=vef->GetSingleObject();
774 if (!sca) return 0;
775 */
776 ScriptedAnimation* sca=gamedata->GetScriptedAnimation(BAM,0);
777 if (!sca) return 0;
778 VEFObject *vef = new VEFObject(sca);
779
780 if(pal) {
781 if (ExtFlags & PEF_TINT) {
782 const auto& pal32 = core->GetPalette32( pal[0] );
783 sca->Tint = pal32[PALSIZE/2];
784 sca->Transparency |= BlitFlags::COLOR_MOD;
785 } else {
786 for(int i=0;i<7;i++) {
787 sca->SetPalette(pal[i], 4+i*PALSIZE);
788 }
789 }
790 }
791 sca->SetOrientation(Orientation);
792 sca->PlayOnce();
793 sca->SetBlend();
794 sca->Pos = Pos;
795 area->AddVVCell(vef);
796 return sca->GetSequenceDuration(AI_UPDATE_TIME);
797 }
798
DoStep(unsigned int walk_speed)799 void Projectile::DoStep(unsigned int walk_speed)
800 {
801 if(pathcounter) {
802 pathcounter--;
803 } else {
804 ClearPath();
805 }
806
807 //intro trailing, drawn only once at the beginning
808 if (pathcounter==0x7ffe) {
809 for(int i=0;i<3;i++) {
810 if(!TrailSpeed[i] && TrailBAM[i][0]) {
811 extension_delay = AddTrail(TrailBAM[i], (ExtFlags&PEF_TINT)?Gradients:NULL);
812 }
813 }
814 }
815
816 if (!path) {
817 ChangePhase();
818 return;
819 }
820
821 if (Pos==Destination) {
822 ClearPath();
823 ChangePhase();
824 return;
825 }
826
827 //don't bug out on 0 smoke frequency like the original IE
828 if ((TFlags&PTF_SMOKE) && SmokeSpeed) {
829 if(!(pathcounter%SmokeSpeed)) {
830 AddTrail(smokebam, SmokeGrad);
831 }
832 }
833
834 for(int i=0;i<3;i++) {
835 if(TrailSpeed[i] && !(pathcounter%TrailSpeed[i])) {
836 AddTrail(TrailBAM[i], (ExtFlags&PEF_TINT)?Gradients:NULL);
837 }
838 }
839
840 if (ExtFlags&PEF_LINE) {
841 if(Extension) {
842 //transform into an explosive line
843 EndTravel();
844 } else {
845 if(!(ExtFlags&PEF_FREEZE) && travel[0]) {
846 //switch to 'fading' phase
847 //SetDelay(travel[0]->GetFrameCount());
848 SetDelay(100);
849 }
850 ChangePhase();
851 }
852 //don't change position
853 return;
854 }
855
856 //path won't be calculated if speed==0
857 walk_speed=1500/walk_speed;
858 ieDword time = core->GetGame()->Ticks;
859 if (!step) {
860 step = path;
861 }
862
863 const PathNode *start = step;
864 while (step->Next && (( time - timeStartStep ) >= walk_speed)) {
865 unsigned int count = Speed;
866 while (step->Next && count) {
867 step = step->Next;
868 --count;
869 }
870 timeStartStep = timeStartStep + walk_speed;
871
872 if (!walk_speed) {
873 break;
874 }
875 }
876
877 if (ExtFlags & PEF_CONTINUE) {
878 // check for every step along the way
879 // the test case is lightning bolt, its a long projectile,
880 LineTarget(start, step->Next);
881 }
882
883 SetOrientation (step->orient, false);
884
885 Pos.x=step->x;
886 Pos.y=step->y;
887 if (travel_handle) {
888 travel_handle->SetPos(Pos.x, Pos.y);
889 }
890 if (!step->Next) {
891 ClearPath();
892 NewOrientation = Orientation;
893 ChangePhase();
894 return;
895 }
896 if (!walk_speed) {
897 return;
898 }
899
900 if (SFlags&PSF_SPARKS) {
901 drawSpark = 1;
902 }
903
904 if (step->Next->x > step->x)
905 Pos.x += ( unsigned short )
906 ( ( step->Next->x - Pos.x ) * ( time - timeStartStep ) / walk_speed );
907 else
908 Pos.x -= ( unsigned short )
909 ( ( Pos.x - step->Next->x ) * ( time - timeStartStep ) / walk_speed );
910 if (step->Next->y > step->y)
911 Pos.y += ( unsigned short )
912 ( ( step->Next->y - Pos.y ) * ( time - timeStartStep ) / walk_speed );
913 else
914 Pos.y -= ( unsigned short )
915 ( ( Pos.y - step->Next->y ) * ( time - timeStartStep ) / walk_speed );
916
917 }
918
SetCaster(ieDword caster,int level)919 void Projectile::SetCaster(ieDword caster, int level)
920 {
921 Caster=caster;
922 Level=level;
923 }
924
GetCaster() const925 ieDword Projectile::GetCaster() const
926 {
927 return Caster;
928 }
929
NextTarget(const Point & p)930 void Projectile::NextTarget(const Point &p)
931 {
932 ClearPath();
933 Destination = p;
934 if (!Speed) {
935 Pos = Destination;
936 return;
937 }
938 NewOrientation = Orientation = GetOrient(Destination, Pos);
939
940 //this hack ensures that the projectile will go away after its time
941 //by the time it reaches this part, it was already expired, so Target
942 //needs to be cleared.
943 if(ExtFlags&PEF_NO_TRAVEL) {
944 Target = 0;
945 Destination = Pos;
946 return;
947 }
948
949 int flags = (ExtFlags&PEF_BOUNCE) ? GL_REBOUND : GL_PASS;
950 int stepping = (ExtFlags & PEF_LINE) ? Speed : 1;
951 path = area->GetLine(Pos, Destination, stepping, Orientation, flags);
952 }
953
SetTarget(const Point & p)954 void Projectile::SetTarget(const Point &p)
955 {
956 Target = 0;
957 NextTarget(p);
958 }
959
SetTarget(ieDword tar,bool fake)960 void Projectile::SetTarget(ieDword tar, bool fake)
961 {
962 Actor *target = NULL;
963
964 if (fake) {
965 Target = 0;
966 FakeTarget = tar;
967 return;
968 } else {
969 Target = tar;
970 target = area->GetActorByGlobalID(tar);
971 }
972
973 if (!target) {
974 phase = P_EXPIRED;
975 return;
976 }
977
978 if (ExtFlags&PEF_CONTINUE) {
979 const Point& A = Origin;
980 const Point& B = target->Pos;
981 double angle = atan2(B.y - A.y, B.x - A.x);
982 double adjustedRange = Feet2Pixels(Range, angle);
983 Point C(A.x + adjustedRange * cos(angle), A.y + adjustedRange * sin(angle));
984 SetTarget(C);
985 } else {
986 //replan the path in case the target moved
987 if(target->Pos!=Destination) {
988 NextTarget(target->Pos);
989 return;
990 }
991
992 //replan the path in case the source moved (only for line projectiles)
993 if(ExtFlags&PEF_LINE) {
994 Actor *c = area->GetActorByGlobalID(Caster);
995 if(c && c->Pos!=Pos) {
996 Pos=c->Pos;
997 NextTarget(target->Pos);
998 }
999 }
1000 }
1001 }
1002
MoveTo(Map * map,const Point & Des)1003 void Projectile::MoveTo(Map *map, const Point &Des)
1004 {
1005 area = map;
1006 Origin = Des;
1007 Pos = Des;
1008 Destination = Des;
1009 }
1010
ClearPath()1011 void Projectile::ClearPath()
1012 {
1013 PathNode* thisNode = path;
1014 while (thisNode) {
1015 PathNode* nextNode = thisNode->Next;
1016 delete( thisNode );
1017 thisNode = nextNode;
1018 }
1019 path = NULL;
1020 step = NULL;
1021 }
1022
CalculateTargetFlag() const1023 int Projectile::CalculateTargetFlag() const
1024 {
1025 //if there are any, then change phase to exploding
1026 int flags = GA_NO_DEAD|GA_NO_UNSCHEDULED;
1027 bool checkingEA = false;
1028
1029 if (Extension) {
1030 if (Extension->AFlags&PAF_NO_WALL) {
1031 flags|=GA_NO_LOS;
1032 }
1033
1034 //projectiles don't affect dead/inanimate normally
1035 if (Extension->AFlags&PAF_INANIMATE) {
1036 flags&=~GA_NO_DEAD;
1037 }
1038
1039 //affect only enemies or allies
1040 switch (Extension->AFlags&PAF_TARGET) {
1041 case PAF_ENEMY:
1042 flags|=GA_NO_NEUTRAL|GA_NO_ALLY;
1043 break;
1044 case PAF_PARTY: //this doesn't exist in IE
1045 flags|=GA_NO_ENEMY;
1046 break;
1047 case PAF_TARGET:
1048 flags|=GA_NO_NEUTRAL|GA_NO_ENEMY;
1049 break;
1050 default:
1051 return flags;
1052 }
1053 if (Extension->AFlags & PAF_TARGET) {
1054 checkingEA = true;
1055 }
1056
1057 //this is the only way to affect neutrals and enemies
1058 if (Extension->APFlags&APF_INVERT_TARGET) {
1059 flags^=(GA_NO_ALLY|GA_NO_ENEMY);
1060 }
1061 }
1062
1063 const Scriptable *caster = area->GetScriptableByGlobalID(Caster);
1064 const Actor *act = nullptr;
1065 if (caster && caster->Type == ST_ACTOR) {
1066 act = (const Actor *) caster;
1067 }
1068 if (caster && (!checkingEA || (act && act->GetStat(IE_EA) < EA_GOODCUTOFF))) {
1069 return flags;
1070 }
1071 // iwd2 ar6050 has doors casting chain lightning :)
1072 if (caster && caster->Type != ST_ACTOR && checkingEA) {
1073 return flags;
1074 }
1075 // make everyone an enemy of neutrals
1076 if (act && checkingEA && act->GetStat(IE_EA) > EA_GOODCUTOFF && act->GetStat(IE_EA) < EA_EVILCUTOFF) {
1077 if ((Extension->AFlags & PAF_TARGET) == PAF_ENEMY) return GA_NO_NEUTRAL | (flags & GA_NO_LOS);
1078 if ((Extension->AFlags & PAF_TARGET) == PAF_TARGET) return GA_NO_ALLY | GA_NO_ENEMY | (flags & GA_NO_LOS);
1079 }
1080
1081 return flags^(GA_NO_ALLY|GA_NO_ENEMY);
1082 }
1083
1084 //get actors covered in area of trigger radius
CheckTrigger(unsigned int radius)1085 void Projectile::CheckTrigger(unsigned int radius)
1086 {
1087 if (phase == P_TRIGGER) {
1088 //special trigger flag, explode only if the trigger animation has
1089 //passed a hardcoded sequence number
1090 if (Extension->AFlags&PAF_TRIGGER_D) {
1091 if (travel[Orientation]) {
1092 int anim = travel[Orientation]->GetCurrentFrameIndex();
1093 if (anim<30)
1094 return;
1095 }
1096 }
1097 }
1098 if (area->GetActorInRadius(Pos, CalculateTargetFlag(), radius)) {
1099 if (phase == P_TRIGGER) {
1100 phase = P_EXPLODING1;
1101 extension_delay = Extension->Delay;
1102 }
1103 } else if (phase == P_EXPLODING1) {
1104 //the explosion is revoked
1105 if (Extension->AFlags&PAF_SYNC) {
1106 phase = P_TRIGGER;
1107 }
1108 }
1109 }
1110
SetEffectsCopy(const EffectQueue * eq,const Point & source)1111 void Projectile::SetEffectsCopy(const EffectQueue *eq, const Point &source)
1112 {
1113 delete effects;
1114 if(!eq) {
1115 effects=NULL;
1116 return;
1117 }
1118 effects = eq->CopySelf();
1119 effects->ModifyAllEffectSources(source);
1120 }
1121
LineTarget() const1122 void Projectile::LineTarget() const
1123 {
1124 LineTarget(path, nullptr);
1125 }
1126
LineTarget(const PathNode * beg,const PathNode * end) const1127 void Projectile::LineTarget(const PathNode *beg, const PathNode *end) const
1128 {
1129 if (!effects) {
1130 return;
1131 }
1132
1133 Actor *original = area->GetActorByGlobalID(Caster);
1134 int targetFlags = CalculateTargetFlag();
1135 const PathNode *iter = beg;
1136
1137 do {
1138 const PathNode *first = iter;
1139 const PathNode *last = iter;
1140 unsigned int orient = first->orient;
1141 while (iter && iter != end && iter->orient == orient) {
1142 last = iter;
1143 iter = iter->Next;
1144 }
1145
1146 const Point s(short(first->x), short(first->y));
1147 const Point d(short(last->x), short(last->y));
1148 const std::vector<Actor *> &actors = area->GetAllActors();
1149
1150 for (Actor *target : actors) {
1151 if (target->GetGlobalID() == Caster) {
1152 continue;
1153 }
1154 if (!target->ValidTarget(targetFlags)) {
1155 continue;
1156 }
1157 double t = 0.0;
1158 if (PersonalLineDistance(s, d, target, &t) > 1) {
1159 continue;
1160 }
1161 if (t < 0.0 && first->Parent != nullptr && first->Parent->orient == orient) {
1162 // skip; assume we've hit the target before
1163 continue;
1164 } else if (t > 1.0 && last->Next != nullptr && last->Next->orient == orient) {
1165 // skip; assume we'll hit it after
1166 continue;
1167 }
1168
1169 if (effects->CheckImmunity(target) > 0) {
1170 EffectQueue *eff = effects->CopySelf();
1171 eff->SetOwner(original);
1172 if (ExtFlags & PEF_RGB) {
1173 target->SetColorMod(0xff, RGBModifier::ADD, ColorSpeed, RGB);
1174 }
1175
1176 eff->AddAllEffects(target, target->Pos);
1177 delete eff;
1178 }
1179 }
1180 } while (iter && iter != end);
1181 }
1182
1183 //secondary projectiles target all in the explosion radius
SecondaryTarget()1184 void Projectile::SecondaryTarget()
1185 {
1186 //fail will become true if the projectile utterly failed to find a target
1187 //if the spell was already applied on explosion, ignore this
1188 bool fail= !!(Extension->APFlags&APF_SPELLFAIL) && !(ExtFlags&PEF_DEFSPELL);
1189 int mindeg = 0;
1190 int maxdeg = 0;
1191 int degOffset = 0;
1192
1193 //the AOE (area of effect) is cone shaped
1194 if (Extension->AFlags&PAF_CONE) {
1195 // see CharAnimations.cpp for a nice visualization of the orientation directions
1196 // they start at 270° and go anticlockwise, so we have to rotate (reflect over y=-x) to match what math functions expect
1197 // TODO: check if we can ignore this and use the angle between caster pos and target pos (are they still available here?)
1198 char saneOrientation = 12 - Orientation;
1199 if (saneOrientation < 0) saneOrientation = MAX_ORIENT + saneOrientation;
1200
1201 // for cone angles (widths) bigger than 22.5 we will always have a range of values greater than 360
1202 // to normalize into [0,360] we use an orientation dependent factor that is then accounted for in later calculations
1203 mindeg = (saneOrientation * (720 / MAX_ORIENT) - Extension->ConeWidth) / 2;
1204 if (mindeg < 0) {
1205 degOffset = -mindeg;
1206 //mindeg = 0;
1207 } else if (mindeg + Extension->ConeWidth > 360) {
1208 degOffset = -(mindeg - 360 + Extension->ConeWidth);
1209 }
1210 mindeg += degOffset;
1211 maxdeg = mindeg + Extension->ConeWidth;
1212 }
1213
1214 if (Extension->DiceCount) {
1215 //precalculate the maximum affected target count in case of PAF_AFFECT_ONE
1216 extension_targetcount = core->Roll(Extension->DiceCount, Extension->DiceSize, 0);
1217 } else {
1218 //this is the default case (for original engine)
1219 extension_targetcount = 1;
1220 }
1221
1222 int radius = Extension->ExplosionRadius / 16;
1223 std::vector<Actor *> actors = area->GetAllActorsInRadius(Pos, CalculateTargetFlag(), radius);
1224 for (const Actor *actor : actors) {
1225 ieDword targetID = actor->GetGlobalID();
1226
1227 //this flag is actually about ignoring the caster (who is at the center)
1228 if ((SFlags & PSF_IGNORE_CENTER) && Caster == targetID) {
1229 continue;
1230 }
1231
1232 //IDS targeting for area projectiles
1233 if (FailedIDS(actor)) {
1234 continue;
1235 }
1236
1237 if (Extension->AFlags&PAF_CONE) {
1238 //cone never affects the caster
1239 if (Caster == targetID) {
1240 continue;
1241 }
1242 double xdiff = actor->Pos.x - Pos.x;
1243 double ydiff = Pos.y - actor->Pos.y;
1244 int deg;
1245
1246 // a dragon will definitely be easier to hit than a mouse
1247 // but nothing checks the personal space of possible targets in the original either #384
1248 if (ydiff) {
1249 // ensure [0,360] range: transform [-180,180] from atan2, but also take orientation correction factor into account
1250 deg = (int) (std::atan2(ydiff, xdiff) * 180/M_PI);
1251 deg = ((deg % 360) + 360 + degOffset) % 360;
1252 } else {
1253 if (xdiff < 0) {
1254 deg = 180;
1255 } else {
1256 deg = 0;
1257 }
1258 }
1259
1260 //not in the right sector of circle
1261 if (mindeg>deg || maxdeg<deg) {
1262 continue;
1263 }
1264 }
1265
1266 Projectile *pro = server->GetProjectileByIndex(Extension->ExplProjIdx);
1267 pro->SetEffectsCopy(effects, Pos);
1268 //copy the additional effects reference to the child projectile
1269 //but only when there is a spell to copy
1270 if (SuccSpell[0])
1271 memcpy(pro->SuccSpell, SuccSpell, sizeof(ieResRef) );
1272 pro->SetCaster(Caster, Level);
1273 //this is needed to apply the success spell on the center point
1274 pro->SetTarget(Pos);
1275 //TODO:actually some of the splash projectiles are a good example of faketarget
1276 //projectiles (that don't follow the target, but still hit)
1277 area->AddProjectile(pro, Pos, targetID, false);
1278 fail=false;
1279
1280 //we already got one target affected in the AOE, this flag says
1281 //that was enough (the GemRB extension can repeat this a random time (x d y)
1282 if(Extension->AFlags&PAF_AFFECT_ONE) {
1283 if (extension_targetcount<=0) {
1284 break;
1285 }
1286 //if target counting is per HD and this target is an actor, use the xp level field
1287 //otherwise count it as one
1288 if (Extension->APFlags & APF_COUNT_HD) {
1289 extension_targetcount -= actor->GetXPLevel(true);
1290 } else {
1291 extension_targetcount--;
1292 }
1293 }
1294 }
1295
1296 //In case of utter failure, apply a spell of the same name on the caster
1297 //this feature is used by SCHARGE, PRTL_OP and PRTL_CL in the HoW pack
1298 if(fail) {
1299 ApplyDefault();
1300 }
1301 }
1302
Update()1303 int Projectile::Update()
1304 {
1305 //if reached target explode
1306 //if target doesn't exist expire
1307 if (phase == P_EXPIRED) {
1308 return 0;
1309 }
1310 if (phase == P_UNINITED) {
1311 Setup();
1312 }
1313
1314 int pause = core->IsFreezed();
1315 if (pause) {
1316 return 1;
1317 }
1318
1319 const Game *game = core->GetGame();
1320 if (game && game->IsTimestopActive() && !(TFlags&PTF_TIMELESS)) {
1321 return 1;
1322 }
1323
1324 //recreate path if target has moved
1325 if(Target) {
1326 SetTarget(Target, false);
1327 }
1328
1329 if (phase == P_TRAVEL || phase == P_TRAVEL2) {
1330 DoStep(Speed);
1331 }
1332 return 1;
1333 }
1334
Draw(const Region & viewport)1335 void Projectile::Draw(const Region& viewport)
1336 {
1337 switch (phase) {
1338 case P_UNINITED:
1339 return;
1340 case P_TRIGGER: case P_EXPLODING1:case P_EXPLODING2:
1341 //This extension flag is to enable the travel projectile at
1342 //trigger/explosion time
1343 if (Extension->AFlags&PAF_VISIBLE) {
1344 //if (!Extension || (Extension->AFlags&PAF_VISIBLE)) {
1345 DrawTravel(viewport);
1346 }
1347 /*
1348 if (!Extension) {
1349 return;
1350 }*/
1351 CheckTrigger(Extension->TriggerRadius);
1352 if (phase == P_EXPLODING1 || phase == P_EXPLODING2) {
1353 DrawExplosion(viewport);
1354 }
1355 break;
1356 case P_TRAVEL: case P_TRAVEL2:
1357 //There is no Extension for simple traveling projectiles!
1358 DrawTravel(viewport);
1359 return;
1360 default:
1361 DrawExploded(viewport);
1362 return;
1363 }
1364 }
1365
DrawChildren(const Region & vp)1366 bool Projectile::DrawChildren(const Region& vp)
1367 {
1368 bool drawn = false;
1369
1370 if (children) {
1371 for(int i=0;i<child_size;i++){
1372 if(children[i]) {
1373 if (children[i]->Update()) {
1374 children[i]->DrawTravel(vp);
1375 drawn = true;
1376 } else {
1377 delete children[i];
1378 children[i]=NULL;
1379 }
1380 }
1381 }
1382 }
1383
1384 return drawn;
1385 }
1386
1387 //draw until all children expire
DrawExploded(const Region & viewport)1388 void Projectile::DrawExploded(const Region& viewport)
1389 {
1390 if (DrawChildren(viewport)) {
1391 return;
1392 }
1393 phase = P_EXPIRED;
1394 }
1395
SpawnFragment(Point & dest)1396 void Projectile::SpawnFragment(Point &dest)
1397 {
1398 Projectile *pro = server->GetProjectileByIndex(Extension->FragProjIdx);
1399 if (pro) {
1400 // if (Extension->AFlags&PAF_SECONDARY) {
1401 // pro->SetEffectsCopy(effects);
1402 // }
1403 pro->SetCaster(Caster, Level);
1404 if (pro->ExtFlags&PEF_RANDOM) {
1405 dest.x+=core->Roll(1,Extension->TileX, -Extension->TileX/2);
1406 dest.y+=core->Roll(1,Extension->TileY, -Extension->TileY/2);
1407 }
1408 area->AddProjectile(pro, dest, dest);
1409 }
1410 }
1411
DrawExplosion(const Region & vp)1412 void Projectile::DrawExplosion(const Region& vp)
1413 {
1414 //This seems to be a needless safeguard
1415 if (!Extension) {
1416 phase = P_EXPIRED;
1417 return;
1418 }
1419
1420 StopSound();
1421 DrawChildren(vp);
1422
1423 int pause = core->IsFreezed();
1424 if (pause) {
1425 return;
1426 }
1427
1428 //Delay explosion, it could even be revoked with PAF_SYNC (see skull trap)
1429 if (extension_delay) {
1430 extension_delay--;
1431 return;
1432 }
1433
1434 //0 and 1 have the same effect (1 explosion)
1435 if (extension_explosioncount) {
1436 extension_explosioncount--;
1437 }
1438
1439 //Line targets are actors between source and destination point
1440 if(ExtFlags&PEF_LINE) {
1441 if (Target) {
1442 SetTarget(Target, false);
1443 }
1444 LineTarget();
1445 }
1446
1447 int apflags = Extension->APFlags;
1448 int aoeflags = Extension->AFlags;
1449
1450 //no idea what is PAF_SECONDARY
1451 //probably it is to alter some behaviour in the secondary
1452 //projectile generation
1453 //In trapskul.pro it isn't set, yet it has a secondary (invisible) projectile
1454 //All area effects are created by secondary projectiles
1455
1456 //the secondary projectile will target everyone in the area of effect
1457 SecondaryTarget();
1458
1459 //draw fragment graphics animation at the explosion center
1460 if (aoeflags&PAF_FRAGMENT) {
1461 //there is a character animation in the center of the explosion
1462 //which will go towards the edges (flames, etc)
1463 //Extension->ExplColor fake color for single shades (blue,green,red flames)
1464 //Extension->FragAnimID the animation id for the character animation
1465 //This color is not used in the original game
1466 Point pos = Pos - vp.Origin();
1467 area->Sparkle(0, Extension->ExplColor, SPARKLE_EXPLOSION, pos, Extension->FragAnimID, GetZPos());
1468 }
1469
1470 if(Shake) {
1471 core->timer.SetScreenShake(Point(Shake, Shake), Shake);
1472 Shake = 0;
1473 }
1474
1475 //the center of the explosion could be another projectile played over the target
1476 //warning: this projectile doesn't inherit any effects, so its payload function
1477 //won't be doing anything (any effect of PAF_SECONDARY?)
1478
1479 //remove PAF_SECONDARY if it is buggy, but that will break the 'HOLD' projectile
1480 if ((Extension->AFlags&PAF_SECONDARY) && Extension->FragProjIdx) {
1481 if (apflags&APF_TILED) {
1482 int i,j;
1483 int radius = Extension->ExplosionRadius;
1484
1485 for (i=-radius;i<radius;i+=Extension->TileX) {
1486 for(j=-radius;j<radius;j+=Extension->TileY) {
1487 if (i*i+j*j<radius*radius) {
1488 Point p(Pos.x+i, Pos.y+j);
1489 SpawnFragment(p);
1490 }
1491 }
1492 }
1493 } else {
1494 SpawnFragment(Pos);
1495 }
1496 }
1497
1498 //the center of the explosion is based on hardcoded explosion type (this is fireball.cpp in the original engine)
1499 //these resources are listed in areapro.2da and served by ProjectileServer.cpp
1500
1501 //draw it only once, at the time of explosion
1502 if (phase==P_EXPLODING1) {
1503 core->GetAudioDrv()->Play(Extension->SoundRes, SFX_CHAN_MISSILE, Pos.x, Pos.y);
1504 //play VVC in center
1505 //FIXME: make it possible to play VEF too?
1506 if (aoeflags&PAF_VVC) {
1507 ScriptedAnimation* vvc = gamedata->GetScriptedAnimation(Extension->VVCRes, false);
1508 if (vvc) {
1509 if (apflags & APF_VVCPAL) {
1510 //if the palette is used as tint (as opposed to clown colorset) tint the vvc
1511 if (apflags & APF_TINT) {
1512 const auto& pal32 = core->GetPalette32(Extension->ExplColor);
1513 vvc->Tint = pal32[PALSIZE/2];
1514 vvc->Transparency |= BlitFlags::COLOR_MOD;
1515 } else {
1516 vvc->SetPalette(Extension->ExplColor);
1517 }
1518 }
1519 //if the trail oriented, then the center is oriented too
1520 if (ExtFlags&PEF_TRAIL) {
1521 vvc->SetOrientation(Orientation);
1522 }
1523 vvc->Pos = Pos;
1524 vvc->PlayOnce();
1525 vvc->SetBlend();
1526 //quick hack to use the single object envelope
1527 area->AddVVCell(new VEFObject(vvc));
1528 }
1529 // bg2 comet has the explosion split into two vvcs, with just a starting cycle difference
1530 // until we actually need two vvc fields in the extension, let's just hack around it
1531 if (!stricmp(Extension->VVCRes, "SPCOMEX1")) {
1532 ScriptedAnimation* vvc = gamedata->GetScriptedAnimation("SPCOMEX2", false);
1533 if (vvc) {
1534 vvc->Pos = Pos;
1535 vvc->PlayOnce();
1536 vvc->SetBlend();
1537 area->AddVVCell(new VEFObject(vvc));
1538 }
1539 }
1540 }
1541
1542 phase=P_EXPLODING2;
1543 } else {
1544 core->GetAudioDrv()->Play(Extension->AreaSound, SFX_CHAN_MISSILE, Pos.x, Pos.y);
1545 }
1546
1547 //the spreading animation is in the first column
1548 const char *tmp = Extension->Spread;
1549 if(tmp[0]) {
1550 //i'm unsure about the need of this
1551 //returns if the explosion animation is fake coloured
1552 if (!children) {
1553 child_size = (Extension->ExplosionRadius+15)/16;
1554 //more sprites if the whole area needs to be filled
1555 if (apflags&APF_FILL) child_size*=2;
1556 if (apflags&APF_SPREAD) child_size*=2;
1557 if (apflags&APF_BOTH) child_size/=2; //intentionally decreases
1558 if (apflags&APF_MORE) child_size*=2;
1559 children = (Projectile **) calloc(sizeof(Projectile *), child_size);
1560 }
1561
1562 //zero cone width means single line area of effect
1563 if((aoeflags&PAF_CONE) && !Extension->ConeWidth) {
1564 child_size = 1;
1565 }
1566
1567 int initial = child_size;
1568
1569 for(int i=0;i<initial;i++) {
1570 //leave this slot free, it is residue from the previous flare up
1571 if (children[i])
1572 continue;
1573 if(apflags&APF_BOTH) {
1574 if(RAND(0,1)) {
1575 tmp = Extension->Secondary;
1576 } else {
1577 tmp = Extension->Spread;
1578 }
1579 }
1580 //create a custom projectile with single traveling effect
1581 Projectile *pro = server->CreateDefaultProjectile((unsigned int) ~0);
1582 strnlwrcpy(pro->BAMRes1, tmp, 8);
1583 if (ExtFlags&PEF_TRAIL) {
1584 pro->Aim = Aim;
1585 }
1586 pro->SetEffects(NULL);
1587 //calculate the child projectile's target point, it is either
1588 //a perimeter or an inside point of the explosion radius
1589 int rad = Extension->ExplosionRadius;
1590 Point newdest;
1591
1592 if (apflags&APF_FILL) {
1593 rad=core->Roll(1,rad,0);
1594 }
1595 int max = 360;
1596 int add = 0;
1597 if (aoeflags&PAF_CONE) {
1598 max=Extension->ConeWidth;
1599 add=(Orientation*45-max)/2;
1600 }
1601 max=core->Roll(1,max,add);
1602 double degree=max*M_PI/180;
1603 newdest.x = (int) -(rad * std::sin(degree) );
1604 newdest.y = (int) (rad * std::cos(degree) );
1605
1606 //these fields and flags are always inherited by all children
1607 pro->Speed = Speed;
1608 pro->ExtFlags = ExtFlags&(PEF_HALFTRANS|PEF_CYCLE|PEF_RGB);
1609 pro->RGB = RGB;
1610 pro->ColorSpeed = ColorSpeed;
1611
1612 if (apflags&APF_FILL) {
1613 int delay;
1614
1615 //a bit of difference in case crowding is needed
1616 //make this a separate flag if speed difference
1617 //is not always wanted
1618 pro->Speed-=RAND(0,7);
1619
1620 delay = Extension->Delay*extension_explosioncount;
1621 if(apflags&APF_BOTH) {
1622 if (delay) {
1623 delay = RAND(0, delay-1);
1624 }
1625 }
1626 //this needs to be commented out for ToB horrid wilting
1627 //if(ExtFlags&PEF_FREEZE) {
1628 delay += Extension->Delay;
1629 //}
1630 pro->SetDelay(delay);
1631 }
1632
1633 newdest.x+=Destination.x;
1634 newdest.y+=Destination.y;
1635
1636 if (apflags&APF_SCATTER) {
1637 pro->MoveTo(area, newdest);
1638 } else {
1639 pro->MoveTo(area, Pos);
1640 }
1641 pro->SetTarget(newdest);
1642 pro->autofree=true;
1643
1644 //sets up the gradient color for the explosion animation
1645 if (apflags&(APF_PALETTE|APF_TINT) ) {
1646 pro->SetGradient(Extension->ExplColor, !(apflags&APF_PALETTE));
1647 }
1648 //i'm unsure if we need blending for all anims or just the tinted ones
1649 pro->TFlags|=PTF_TRANS;
1650 //random frame is needed only for some of these, make it an areapro flag?
1651 if( !(ExtFlags&PEF_CYCLE) || (ExtFlags&PEF_RANDOM) ) {
1652 pro->ExtFlags|=PEF_RANDOM;
1653 }
1654
1655 pro->Setup();
1656
1657 // currently needed by bg2/how Web (less obvious in bg1)
1658 // TODO: original behaviour was: play once, wait in final frame, hide, wait, repeat
1659 if (pro->travel[0] && Extension->APFlags & APF_PLAYONCE) {
1660 // set on all orients while we don't force one for single-orientation animations (see CreateOrientedAnimations)
1661 for (int i=0; i<MAX_ORIENT; i++) {
1662 if (pro->travel[i]) pro->travel[i]->Flags |= A_ANI_PLAYONCE;
1663 }
1664 }
1665
1666 children[i]=pro;
1667 }
1668 }
1669
1670 if (extension_explosioncount) {
1671 extension_delay=Extension->Delay;
1672 } else {
1673 phase = P_EXPLODED;
1674 }
1675 }
1676
GetTravelPos(int face) const1677 int Projectile::GetTravelPos(int face) const
1678 {
1679 if (travel[face]) {
1680 return travel[face]->GetCurrentFrameIndex();
1681 }
1682 return 0;
1683 }
1684
GetShadowPos(int face) const1685 int Projectile::GetShadowPos(int face) const
1686 {
1687 if (shadow[face]) {
1688 return shadow[face]->GetCurrentFrameIndex();
1689 }
1690 return 0;
1691 }
1692
SetPos(int face,int frame1,int frame2)1693 void Projectile::SetPos(int face, int frame1, int frame2)
1694 {
1695 if (travel[face]) {
1696 travel[face]->SetPos(frame1);
1697 }
1698 if (shadow[face]) {
1699 shadow[face]->SetPos(frame2);
1700 }
1701 }
1702
1703 //recalculate target and source points (perpendicular bisector)
SetupWall()1704 void Projectile::SetupWall()
1705 {
1706 Point p1, p2;
1707
1708 p1.x=(Pos.x+Destination.x)/2;
1709 p1.y=(Pos.y+Destination.y)/2;
1710
1711 p2.x=p1.x+(Pos.y-Destination.y);
1712 p2.y=p1.y+(Pos.x-Destination.x);
1713 Pos=p1;
1714 SetTarget(p2);
1715 }
1716
DrawLine(const Region & vp,int face,BlitFlags flag)1717 void Projectile::DrawLine(const Region& vp, int face, BlitFlags flag)
1718 {
1719 Game *game = core->GetGame();
1720 PathNode *iter = path;
1721 Holder<Sprite2D> frame;
1722 if (game && game->IsTimestopActive() && !(TFlags&PTF_TIMELESS)) {
1723 frame = travel[face]->LastFrame();
1724 flag |= BlitFlags::GREY;
1725 } else {
1726 frame = travel[face]->NextFrame();
1727 }
1728
1729 Color tint2 = tint;
1730 if (game) game->ApplyGlobalTint(tint2, flag);
1731 while(iter) {
1732 Point pos(iter->x - vp.x, iter->y - vp.y);
1733
1734 if (SFlags&PSF_FLYING) {
1735 pos.y-=FLY_HEIGHT;
1736 }
1737
1738 Draw(frame, pos, flag, tint2);
1739 iter = iter->Next;
1740 }
1741 }
1742
DrawTravel(const Region & viewport)1743 void Projectile::DrawTravel(const Region& viewport)
1744 {
1745 Game *game = core->GetGame();
1746 BlitFlags flag;
1747
1748 if(ExtFlags&PEF_HALFTRANS) {
1749 flag = BlitFlags::HALFTRANS;
1750 } else {
1751 flag = BlitFlags::NONE;
1752 }
1753
1754 //static tint (use the tint field)
1755 if(ExtFlags&PEF_TINT) {
1756 flag |= BlitFlags::COLOR_MOD;
1757 }
1758
1759 //Area tint
1760 if (TFlags&PTF_TINT) {
1761 tint = area->LightMap->GetPixel( Pos.x / 16, Pos.y / 12);
1762 tint.a = 255;
1763 flag |= BlitFlags::COLOR_MOD;
1764 }
1765
1766 unsigned int face = GetNextFace();
1767 if (face!=Orientation) {
1768 SetPos(face, GetTravelPos(face), GetShadowPos(face));
1769 }
1770
1771 Point pos = Pos - viewport.Origin();
1772 if(bend && phase == P_TRAVEL && Origin != Destination) {
1773 double total_distance = Distance(Origin, Destination);
1774 double travelled_distance = Distance(Origin, Pos);
1775
1776 // distance travelled along the line, from 0.0 to 1.0
1777 double travelled = travelled_distance / total_distance;
1778 assert(travelled <= 1.0);
1779
1780 // input to sin(): 0 to pi gives us an arc
1781 double arc_angle = travelled * M_PI;
1782
1783 // calculate the distance between the arc and the current pos
1784 // (this could use travelled and a larger constant multiplier,
1785 // to make the arc size fixed rather than relative to the total
1786 // distance to travel)
1787 double length_of_normal = travelled_distance * std::sin(arc_angle) * 0.3 * ((bend / 2) + 1);
1788 if (bend % 2) length_of_normal = -length_of_normal;
1789
1790 // adjust the to-be-rendered point by that distance
1791 double x_vector = (Destination.x - Origin.x) / total_distance,
1792 y_vector = (Destination.y - Origin.y) / total_distance;
1793 Point newpos = pos;
1794 newpos.x += (short)(y_vector*length_of_normal);
1795 newpos.y -= (short)(x_vector*length_of_normal);
1796 pos = newpos;
1797 }
1798
1799 // set up the tint for the rest of the blits, but don't overwrite the saved one
1800 Color tint2 = tint;
1801 BlitFlags flags = flag;
1802
1803 if (TFlags&PTF_TINT && game) {
1804 game->ApplyGlobalTint(tint2, flags);
1805 }
1806
1807 if (light) {
1808 Draw(light, pos, flags^flag, tint2);
1809 }
1810
1811 if (ExtFlags&PEF_POP) {
1812 //draw pop in/hold/pop out animation sequences
1813 Holder<Sprite2D> frame;
1814 if (game && game->IsTimestopActive() && !(TFlags&PTF_TIMELESS)) {
1815 frame = travel[face]->LastFrame();
1816 flags |= BlitFlags::GREY;
1817 } else {
1818 if (ExtFlags&PEF_UNPOP) {
1819 frame = shadow[0]->NextFrame();
1820 if (shadow[0]->endReached) {
1821 ExtFlags &= ~PEF_UNPOP;
1822 }
1823 } else {
1824 frame = travel[0]->NextFrame();
1825 if (travel[0]->endReached) {
1826 travel[0]->playReversed = true;
1827 travel[0]->SetPos(0);
1828 ExtFlags |= PEF_UNPOP;
1829 frame = shadow[0]->NextFrame();
1830 }
1831 }
1832 }
1833 Draw(frame, pos, flags, tint2);
1834 return;
1835 }
1836
1837 if (ExtFlags&PEF_LINE) {
1838 DrawLine(viewport, face, flag);
1839 return;
1840 }
1841
1842 if (shadow[face]) {
1843 Holder<Sprite2D> frame = shadow[face]->NextFrame();
1844 Draw(frame, pos, flags, tint2);
1845 }
1846
1847 pos.y-=GetZPos();
1848
1849 if (ExtFlags&PEF_PILLAR) {
1850 //draw all frames simultaneously on top of each other
1851 for(int i=0;i<Aim;i++) {
1852 if (travel[i]) {
1853 Holder<Sprite2D> frame = travel[i]->NextFrame();
1854 Draw(frame, pos, flags, tint2);
1855 pos.y-=frame->Frame.y;
1856 }
1857 }
1858 } else {
1859 if (travel[face]) {
1860 Holder<Sprite2D> frame;
1861 if (game && game->IsTimestopActive() && !(TFlags&PTF_TIMELESS)) {
1862 frame = travel[face]->LastFrame();
1863 flags |= BlitFlags::GREY; // move higher if it interferes with other tints badly
1864 } else {
1865 frame = travel[face]->NextFrame();
1866 }
1867 Draw(frame, pos, flags, tint2);
1868 }
1869 }
1870
1871 if (drawSpark) {
1872 area->Sparkle(0,SparkColor, SPARKLE_EXPLOSION, pos, 0, GetZPos() );
1873 drawSpark = 0;
1874 }
1875
1876 }
1877
GetZPos() const1878 int Projectile::GetZPos() const
1879 {
1880 return ZPos;
1881 }
1882
SetIdentifiers(const char * resref,ieWord id)1883 void Projectile::SetIdentifiers(const char *resref, ieWord id)
1884 {
1885 strnuprcpy(name, resref, 8);
1886 type=id;
1887 }
1888
PointInRadius(const Point & p) const1889 bool Projectile::PointInRadius(const Point &p) const
1890 {
1891 switch(phase) {
1892 //better not trigger on projectiles unset/expired
1893 case P_EXPIRED:
1894 case P_UNINITED: return false;
1895 case P_TRAVEL:
1896 if(p.x==Pos.x && p.y==Pos.y) return true;
1897 return false;
1898 default:
1899 if(p.x==Pos.x && p.y==Pos.y) return true;
1900 if (!Extension) return false;
1901 if (Distance(p,Pos)<Extension->ExplosionRadius) return true;
1902 }
1903 return false;
1904 }
1905
1906 //Set gradient color, if type is true then it is static tint, otherwise it is paletted color
SetGradient(int gradient,bool type)1907 void Projectile::SetGradient(int gradient, bool type)
1908 {
1909 //gradients are unsigned chars, so this works
1910 memset(Gradients, gradient, 7);
1911 if (type) {
1912 ExtFlags|=PEF_TINT;
1913 } else {
1914 TFlags |= PTF_COLOUR;
1915 }
1916 }
1917
StaticTint(const Color & newtint)1918 void Projectile::StaticTint(const Color &newtint)
1919 {
1920 tint = newtint;
1921 TFlags &= ~PTF_TINT; //turn off area tint
1922 }
1923
GetPhase() const1924 int Projectile::GetPhase() const
1925 {
1926 return phase;
1927 }
1928
Cleanup()1929 void Projectile::Cleanup()
1930 {
1931 //neutralise the payload
1932 delete effects;
1933 effects = NULL;
1934 //diffuse the projectile
1935 phase=P_EXPIRED;
1936 }
1937
Draw(Holder<Sprite2D> spr,const Point & p,BlitFlags flags,Color tint) const1938 void Projectile::Draw(Holder<Sprite2D> spr, const Point& p, BlitFlags flags, Color tint) const
1939 {
1940 Video *video = core->GetVideoDriver();
1941 PaletteHolder pal = (spr->Bpp == 8) ? palette : nullptr;
1942 if (flags & BlitFlags::COLOR_MOD) {
1943 // FIXME: this may not apply universally
1944 flags |= BlitFlags::ALPHA_MOD;
1945 }
1946 video->BlitGameSpriteWithPalette(spr, pal, p, flags|BlitFlags::BLENDED, tint);
1947 }
1948
1949 }
1950