1 //Copyright Paul Reiche, Fred Ford. 1992-2002
2 
3 /*
4  *  This program is free software; you can redistribute it and/or modify
5  *  it under the terms of the GNU General Public License as published by
6  *  the Free Software Foundation; either version 2 of the License, or
7  *  (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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17  */
18 
19 #define COMM_INTERNAL
20 #include "commanim.h"
21 
22 #include "comm.h"
23 #include "element.h"
24 #include "setup.h"
25 #include "libs/compiler.h"
26 #include "libs/graphics/cmap.h"
27 #include "libs/mathlib.h"
28 
29 
30 static TimeCount LastTime;
31 static SEQUENCE Sequences[MAX_ANIMATIONS + 2];
32 		// 2 extra for Talk and Transition animations
33 static DWORD ActiveMask;
34 		// Bit mask of all animations that are currently active.
35 		// Bit 'i' is set if the animation with index 'i' is active.
36 static ANIMATION_DESC TalkDesc;
37 static ANIMATION_DESC TransitDesc;
38 static SEQUENCE* Talk;
39 static SEQUENCE* Transit;
40 static COUNT FirstAmbient;
41 static COUNT TotalSequences;
42 
43 
44 static inline DWORD
randomFrameRate(SEQUENCE * pSeq)45 randomFrameRate (SEQUENCE *pSeq)
46 {
47 	ANIMATION_DESC *ADPtr = pSeq->ADPtr;
48 
49 	return ADPtr->BaseFrameRate	+
50 			TFB_Random () % (ADPtr->RandomFrameRate + 1);
51 }
52 
53 static inline DWORD
randomRestartRate(SEQUENCE * pSeq)54 randomRestartRate (SEQUENCE *pSeq)
55 {
56 	ANIMATION_DESC *ADPtr = pSeq->ADPtr;
57 
58 	return ADPtr->BaseRestartRate +
59 			TFB_Random () % (ADPtr->RandomRestartRate + 1);
60 }
61 
62 static inline COUNT
randomFrameIndex(SEQUENCE * pSeq,COUNT from)63 randomFrameIndex (SEQUENCE *pSeq, COUNT from)
64 {
65 	ANIMATION_DESC *ADPtr = pSeq->ADPtr;
66 
67 	return from	+ TFB_Random () % (ADPtr->NumFrames - from);
68 }
69 
70 static void
SetupAmbientSequences(SEQUENCE * pSeq,COUNT Num)71 SetupAmbientSequences (SEQUENCE *pSeq, COUNT Num)
72 {
73 	COUNT i;
74 
75 	for (i = 0; i < Num; ++i, ++pSeq)
76 	{
77 		ANIMATION_DESC *ADPtr = &CommData.AlienAmbientArray[i];
78 
79 		memset (pSeq, 0, sizeof (*pSeq));
80 
81 		pSeq->ADPtr = ADPtr;
82 		if (ADPtr->AnimFlags & COLORXFORM_ANIM)
83 			pSeq->AnimType = COLOR_ANIM;
84 		else
85 			pSeq->AnimType = PICTURE_ANIM;
86 		pSeq->Direction = UP_DIR;
87 		pSeq->FramesLeft = ADPtr->NumFrames;
88 		// Default: first frame is neutral
89 		if (ADPtr->AnimFlags & RANDOM_ANIM)
90 		{	// Set a random frame/colormap
91 			pSeq->NextIndex = TFB_Random () % ADPtr->NumFrames;
92 		}
93 		else if (ADPtr->AnimFlags & YOYO_ANIM)
94 		{	// Skip the first frame/colormap (it's neutral)
95 			pSeq->NextIndex = 1;
96 			--pSeq->FramesLeft;
97 		}
98 		else if (ADPtr->AnimFlags & CIRCULAR_ANIM)
99 		{	// Exception that makes everything more painful:
100 			// *Last* frame is neutral
101 			pSeq->CurIndex = ADPtr->NumFrames - 1;
102 			pSeq->NextIndex = 0;
103 		}
104 
105 		pSeq->Alarm = randomRestartRate (pSeq) + 1;
106 	}
107 }
108 
109 static void
SetupTalkSequence(SEQUENCE * pSeq,ANIMATION_DESC * ADPtr)110 SetupTalkSequence (SEQUENCE *pSeq, ANIMATION_DESC *ADPtr)
111 {
112 	memset (pSeq, 0, sizeof (*pSeq));
113 	// Initially disabled, and until needed
114 	ADPtr->AnimFlags |= ANIM_DISABLED;
115 	pSeq->ADPtr = ADPtr;
116 	pSeq->AnimType = PICTURE_ANIM;
117 }
118 
119 static inline BOOLEAN
animAtNeutralIndex(SEQUENCE * pSeq)120 animAtNeutralIndex (SEQUENCE *pSeq)
121 {
122 	ANIMATION_DESC *ADPtr = pSeq->ADPtr;
123 
124 	if (ADPtr->AnimFlags & CIRCULAR_ANIM)
125 	{	// CIRCULAR_ANIM's neutral frame is the last
126 		return pSeq->NextIndex == 0;
127 	}
128 	else
129 	{	// All others, neutral frame is the first
130 		return pSeq->CurIndex == 0;
131 	}
132 }
133 
134 static inline BOOLEAN
conflictsWithTalkingAnim(SEQUENCE * pSeq)135 conflictsWithTalkingAnim (SEQUENCE *pSeq)
136 {
137 	ANIMATION_DESC *ADPtr = pSeq->ADPtr;
138 
139 	return ADPtr->AnimFlags & CommData.AlienTalkDesc.AnimFlags & WAIT_TALKING;
140 }
141 
142 static void
ProcessColormapAnims(SEQUENCE * pSeq,COUNT Num)143 ProcessColormapAnims (SEQUENCE *pSeq, COUNT Num)
144 {
145 	COUNT i;
146 
147 	for (i = 0; i < Num; ++i, ++pSeq)
148 	{
149 		ANIMATION_DESC *ADPtr = pSeq->ADPtr;
150 
151 		if ((ADPtr->AnimFlags & ANIM_DISABLED)
152 				|| pSeq->AnimType != COLOR_ANIM
153 				|| !pSeq->Change)
154 			continue;
155 
156 		XFormColorMap (GetColorMapAddress (
157 				SetAbsColorMapIndex (CommData.AlienColorMap,
158 				ADPtr->StartIndex + pSeq->CurIndex)),
159 				pSeq->Alarm - 1);
160 		pSeq->Change = FALSE;
161 	}
162 }
163 
164 static BOOLEAN
AdvanceAmbientSequence(SEQUENCE * pSeq)165 AdvanceAmbientSequence (SEQUENCE *pSeq)
166 {
167 	BOOLEAN active;
168 	ANIMATION_DESC *ADPtr = pSeq->ADPtr;
169 
170 	--pSeq->FramesLeft;
171 	// YOYO_ANIM does not actually end until it comes back
172 	// in reverse direction, even if FramesLeft gets to 0 here
173 	if (pSeq->FramesLeft
174 			|| ((ADPtr->AnimFlags & YOYO_ANIM) && pSeq->NextIndex != 0))
175 	{
176 		active = TRUE;
177 		pSeq->Alarm = randomFrameRate (pSeq) + 1;
178 	}
179 	else
180 	{	// last animation frame
181 		active = FALSE;
182 		pSeq->Alarm = randomRestartRate (pSeq) + 1;
183 
184 		// RANDOM_ANIM must end on a neutral frame
185 		if (ADPtr->AnimFlags & RANDOM_ANIM)
186 			pSeq->NextIndex = 0;
187 	}
188 
189 	// Will draw the next frame or change to next colormap
190 	pSeq->CurIndex = pSeq->NextIndex;
191 	pSeq->Change = TRUE;
192 
193 	if (pSeq->FramesLeft == 0)
194 	{	// Animation ended
195 		// Set it up for the next round
196 		pSeq->FramesLeft = ADPtr->NumFrames;
197 
198 		if (ADPtr->AnimFlags & YOYO_ANIM)
199 		{	// YOYO_ANIM never draws the first frame
200 			// ("first" depends on direction)
201 			--pSeq->FramesLeft;
202 			pSeq->Direction = -pSeq->Direction;
203 		}
204 		else if (ADPtr->AnimFlags & CIRCULAR_ANIM)
205 		{	// Rewind the CIRCULAR_ANIM
206 			// NextIndex will be brought to 0 just below
207 			pSeq->NextIndex = -1;
208 		}
209 		// RANDOM_ANIM is setup just below
210 	}
211 
212 	if (ADPtr->AnimFlags & RANDOM_ANIM)
213 		pSeq->NextIndex = randomFrameIndex (pSeq, 0);
214 	else
215 		pSeq->NextIndex += pSeq->Direction;
216 
217 	return active;
218 }
219 
220 static void
ResetSequence(SEQUENCE * pSeq)221 ResetSequence (SEQUENCE *pSeq)
222 {
223 	// Reset the animation and cause a redraw of the neutral frame,
224 	// assuming it is not ANIM_DISABLED
225 	// NOTE: This does not handle CIRCULAR_ANIM properly
226 	pSeq->Direction = NO_DIR;
227 	pSeq->CurIndex = 0;
228 	pSeq->Change = TRUE;
229 }
230 
231 static void
AdvanceTalkingSequence(SEQUENCE * pSeq,DWORD ElapsedTicks)232 AdvanceTalkingSequence (SEQUENCE *pSeq, DWORD ElapsedTicks)
233 {
234 	// We use the actual descriptor for flags processing and
235 	// a copied one for drawing. A copied one is updated only
236 	// when it is safe to do so.
237 	ANIMATION_DESC *ADPtr = pSeq->ADPtr;
238 
239 	if (pSeq->Direction == NO_DIR)
240 	{	// just starting now
241 		pSeq->Direction = UP_DIR;
242 		// It's now safe to pick up new Talk descriptor if changed
243 		// (e.g. Zoq and Pik taking turns to talk)
244 		if (CommData.AlienTalkDesc.StartIndex != ADPtr->StartIndex)
245 		{	// copy the new one
246 			*ADPtr = CommData.AlienTalkDesc;
247 		}
248 
249 		assert (pSeq->CurIndex == 0);
250 		pSeq->Alarm = 0; // now!
251 		ADPtr->AnimFlags &= ~ANIM_DISABLED;
252 	}
253 
254 	if (pSeq->Alarm > ElapsedTicks)
255 	{	// Not time yet
256 		pSeq->Alarm -= ElapsedTicks;
257 		return;
258 	}
259 
260 	// Time to start or advance the animation
261 	pSeq->Alarm = randomFrameRate (pSeq);
262 	pSeq->Change = TRUE;
263 	// Talking animation is like RANDOM_ANIM, except that
264 	// random frames always alternate with the neutral one
265 	// The animation does not stop until we reset it
266 	if (pSeq->CurIndex == 0)
267 	{	// random frame next
268 		pSeq->CurIndex = randomFrameIndex (pSeq, 1);
269 		pSeq->Alarm += randomRestartRate (pSeq);
270 	}
271 	else
272 	{	// neutral frame next
273 		pSeq->CurIndex = 0;
274 	}
275 }
276 
277 static BOOLEAN
AdvanceTransitSequence(SEQUENCE * pSeq,DWORD ElapsedTicks)278 AdvanceTransitSequence (SEQUENCE *pSeq, DWORD ElapsedTicks)
279 {
280 	BOOLEAN done = FALSE;
281 	// We use the actual descriptor for flags processing and
282 	// a copied one for drawing. A copied one is updated only
283 	// when it is safe to do so.
284 	ANIMATION_DESC *ADPtr = pSeq->ADPtr;
285 
286 	if (pSeq->Direction == NO_DIR)
287 	{	// just starting now
288 		pSeq->Alarm = 0; // now!
289 		ADPtr->AnimFlags &= ~ANIM_DISABLED;
290 	}
291 
292 	if (pSeq->Alarm > ElapsedTicks)
293 	{	// Not time yet
294 		pSeq->Alarm -= ElapsedTicks;
295 		return FALSE;
296 	}
297 
298 	// Time to start or advance the animation
299 	pSeq->Change = TRUE;
300 
301 	if (pSeq->Direction == NO_DIR)
302 	{	// just starting now
303 		pSeq->FramesLeft = ADPtr->NumFrames;
304 		// Both INTRO and DONE may be set at the same time,
305 		// when e.g. Zoq and Pik are taking turns to talk
306 		// Process the DONE transition first to go into
307 		// a neutral state before switching over.
308 		if (CommData.AlienTransitionDesc.AnimFlags & TALK_DONE)
309 		{
310 			pSeq->Direction = DOWN_DIR;
311 			pSeq->CurIndex = ADPtr->NumFrames - 1;
312 		}
313 		else if (CommData.AlienTransitionDesc.AnimFlags & TALK_INTRO)
314 		{
315 			pSeq->Direction = UP_DIR;
316 			// It's now safe to pick up new Transition descriptor if changed
317 			// (e.g. Zoq and Pik taking turns to talk)
318 			if (CommData.AlienTransitionDesc.StartIndex
319 					!= ADPtr->StartIndex)
320 			{	// copy the new one
321 				*ADPtr = CommData.AlienTransitionDesc;
322 			}
323 
324 			pSeq->CurIndex = 0;
325 		}
326 	}
327 
328 	--pSeq->FramesLeft;
329 	if (pSeq->FramesLeft == 0)
330 	{	// animation is done
331 		if (pSeq->Direction == UP_DIR)
332 		{	// done with TALK_INTRO transition
333 			CommData.AlienTransitionDesc.AnimFlags &= ~TALK_INTRO;
334 		}
335 		else if (pSeq->Direction == DOWN_DIR)
336 		{	// done with TALK_DONE transition
337 			CommData.AlienTransitionDesc.AnimFlags &= ~TALK_DONE;
338 
339 			// Done with all transition frames
340 			ADPtr->AnimFlags |= ANIM_DISABLED;
341 			done = TRUE;
342 		}
343 		pSeq->Direction = NO_DIR;
344 	}
345 	else
346 	{	// next frame
347 		pSeq->Alarm = randomFrameRate (pSeq);
348 		pSeq->CurIndex += pSeq->Direction;
349 	}
350 
351 	return done;
352 }
353 
354 void
InitCommAnimations(void)355 InitCommAnimations (void)
356 {
357 	ActiveMask = 0;
358 
359 	TalkDesc = CommData.AlienTalkDesc;
360 	TransitDesc = CommData.AlienTransitionDesc;
361 
362 	// Animation sequences have to be drawn in reverse, and
363 	// talk animations have to be drawn last (so we add them first)
364 	TotalSequences = 0;
365 	// Transition animation last
366 	Transit = Sequences + TotalSequences;
367 	SetupTalkSequence (Transit, &TransitDesc);
368 	++TotalSequences;
369 	// Talk animation second last
370 	Talk = Sequences + TotalSequences;
371 	SetupTalkSequence (Talk, &TalkDesc);
372 	++TotalSequences;
373 	FirstAmbient = TotalSequences;
374 	SetupAmbientSequences (Sequences + FirstAmbient, CommData.NumAnimations);
375 	TotalSequences += CommData.NumAnimations;
376 
377 	LastTime = GetTimeCounter ();
378 }
379 
380 BOOLEAN
ProcessCommAnimations(BOOLEAN FullRedraw,BOOLEAN paused)381 ProcessCommAnimations (BOOLEAN FullRedraw, BOOLEAN paused)
382 {
383 	if (paused)
384 	{	// Drive colormap xforms and nothing else
385 		XFormColorMap_step ();
386 		return FALSE;
387 	}
388 	else
389 	{
390 		COUNT i;
391 		SEQUENCE *pSeq;
392 		BOOLEAN Change;
393 		BOOLEAN CanTalk = TRUE;
394 		TimeCount CurTime;
395 		DWORD ElapsedTicks;
396 		DWORD NextActiveMask;
397 
398 		CurTime = GetTimeCounter ();
399 		ElapsedTicks = CurTime - LastTime;
400 		LastTime = CurTime;
401 
402 		// Process ambient animations
403 		NextActiveMask = ActiveMask;
404 		pSeq = Sequences + FirstAmbient;
405 		for (i = 0; i < CommData.NumAnimations; ++i, ++pSeq)
406 		{
407 			ANIMATION_DESC *ADPtr = pSeq->ADPtr;
408 			DWORD ActiveBit = 1L << i;
409 
410 			if (ADPtr->AnimFlags & ANIM_DISABLED)
411 				continue;
412 
413 			if (pSeq->Direction == NO_DIR)
414 			{	// animation is paused
415 				if (!conflictsWithTalkingAnim (pSeq))
416 				{	// start it up
417 					pSeq->Direction = UP_DIR;
418 				}
419 			}
420 			else if (pSeq->Alarm > ElapsedTicks)
421 			{	// not time yet
422 				pSeq->Alarm -= ElapsedTicks;
423 			}
424 			else if (ActiveMask & ADPtr->BlockMask)
425 			{	// animation is blocked
426 				assert (!(ActiveMask & ActiveBit) &&
427 						"Check animations' mutual blocking masks");
428 				assert (animAtNeutralIndex (pSeq));
429 				// reschedule
430 				pSeq->Alarm = randomRestartRate (pSeq) + 1;
431 				continue;
432 			}
433 			else
434 			{	// Time to start or advance the animation
435 				if (AdvanceAmbientSequence (pSeq))
436 				{	// Animation is active this frame and the next
437 					ActiveMask |= ActiveBit;
438 					NextActiveMask |= ActiveBit;
439 				}
440 				else
441 				{	// Animation remains active this frame but not the next
442 					// This keeps any conflicting animations (BlockMask)
443 					// from activating in the same frame and scribbling over
444 					// our last image.
445 					NextActiveMask &= ~ActiveBit;
446 				}
447 			}
448 
449 			if (pSeq->AnimType == PICTURE_ANIM && pSeq->Direction != NO_DIR
450 					&& conflictsWithTalkingAnim (pSeq))
451 			{
452 				// We want to talk, but this is a running picture animation
453 				// which conflicts with the talking animation
454 				// See if it is safe to stop it now.
455 				if (animAtNeutralIndex (pSeq))
456 				{	// pause the animation
457 					pSeq->Direction = NO_DIR;
458 					NextActiveMask &= ~ActiveBit;
459 					// Talk animation is drawn last, so it's not a conflict
460 					// for this frame. The talk animation will be drawn
461 					// over the neutral frame.
462 				}
463 				else
464 				{	// Otherwise, let the animation run until it's safe
465 					CanTalk = FALSE;
466 				}
467 			}
468 		}
469 		// All ambient animations have been processed. Advance the mask.
470 		ActiveMask = NextActiveMask;
471 
472 		// Process the talking and transition animations
473 		if (CanTalk	&& haveTalkingAnim () && runningTalkingAnim ())
474 		{
475 			BOOLEAN done = FALSE;
476 
477 			if (signaledStopTalkingAnim () && haveTransitionAnim ())
478 			{	// Run the transition. We will clear everything
479 				// when it is done
480 				CommData.AlienTransitionDesc.AnimFlags |= TALK_DONE;
481 			}
482 
483 			if (CommData.AlienTransitionDesc.AnimFlags
484 					& (TALK_INTRO | TALK_DONE))
485 			{	// Transitioning in or out of talking
486 				if ((CommData.AlienTransitionDesc.AnimFlags & TALK_DONE)
487 						&& Transit->Direction == NO_DIR)
488 				{	// This is needed when switching talking anims
489 					ResetSequence (Talk);
490 				}
491 				done = AdvanceTransitSequence (Transit, ElapsedTicks);
492 			}
493 			else if (!signaledStopTalkingAnim ())
494 			{	// Talking, transition is done
495 				AdvanceTalkingSequence (Talk, ElapsedTicks);
496 			}
497 			else
498 			{	// Not talking
499 				ResetSequence (Talk);
500 				done = TRUE;
501 			}
502 
503 			if (signaledStopTalkingAnim () && done)
504 			{
505 				clearRunTalkingAnim ();
506 				clearStopTalkingAnim ();
507 			}
508 		}
509 		else
510 		{	// Not talking -- disable talking anim if it is done
511 			if (Talk->Direction == NO_DIR)
512 				TalkDesc.AnimFlags |= ANIM_DISABLED;
513 		}
514 
515 		BatchGraphics ();
516 
517 		// Draw all animations
518 		{
519 			BOOLEAN ColorChange = XFormColorMap_step ();
520 
521 			if (ColorChange)
522 				FullRedraw = TRUE;
523 
524 			// Colormap animations are processed separately
525 			// from picture anims (see XFormColorMap_step)
526 			ProcessColormapAnims (Sequences + FirstAmbient,
527 					CommData.NumAnimations);
528 
529 			Change = DrawAlienFrame (Sequences, TotalSequences, FullRedraw);
530 			if (FullRedraw)
531 				Change = TRUE;
532 		}
533 
534 		UnbatchGraphics ();
535 
536 		// Post-process ambient animations
537 		pSeq = Sequences + FirstAmbient;
538 		for (i = 0; i < CommData.NumAnimations; ++i, ++pSeq)
539 		{
540 			ANIMATION_DESC *ADPtr = pSeq->ADPtr;
541 			DWORD ActiveBit = 1L << i;
542 
543 			if (ADPtr->AnimFlags & ANIM_DISABLED)
544 				continue;
545 
546 			// We can only disable a one-shot anim here, otherwise the
547 			// last frame will not be drawn
548 			if ((ADPtr->AnimFlags & ONE_SHOT_ANIM)
549 					&& !(NextActiveMask & ActiveBit))
550 			{	// One-shot animation, inactive next frame
551 				ADPtr->AnimFlags |= ANIM_DISABLED;
552 			}
553 		}
554 
555 		return Change;
556 	}
557 }
558 
559 BOOLEAN
DrawAlienFrame(SEQUENCE * Sequences,COUNT Num,BOOLEAN fullRedraw)560 DrawAlienFrame (SEQUENCE *Sequences, COUNT Num, BOOLEAN fullRedraw)
561 {
562 	int i;
563 	STAMP s;
564 	BOOLEAN Change = FALSE;
565 
566 	BatchGraphics ();
567 
568 	s.origin.x = -SAFE_X;
569 	s.origin.y = 0;
570 
571 	if (fullRedraw)
572 	{
573 		// Draw the main frame
574 		s.frame = CommData.AlienFrame;
575 		DrawStamp (&s);
576 
577 		// Draw any static frames (has to be in reverse)
578 		for (i = CommData.NumAnimations - 1; i >= 0; --i)
579 		{
580 			ANIMATION_DESC *ADPtr = &CommData.AlienAmbientArray[i];
581 
582 			if (ADPtr->AnimFlags & ANIM_MASK)
583 				continue;
584 
585 			ADPtr->AnimFlags |= ANIM_DISABLED;
586 
587 			if (!(ADPtr->AnimFlags & COLORXFORM_ANIM))
588 			{	// It's a static frame (e.g. Flagship picture at Starbase)
589 				s.frame = SetAbsFrameIndex (CommData.AlienFrame,
590 						ADPtr->StartIndex);
591 				DrawStamp (&s);
592 			}
593 		}
594 	}
595 
596 	if (Sequences)
597 	{	// Draw the animation sequences (has to be in reverse)
598 		for (i = Num - 1; i >= 0; --i)
599 		{
600 			SEQUENCE *pSeq = &Sequences[i];
601 			ANIMATION_DESC *ADPtr = pSeq->ADPtr;
602 
603 			if ((ADPtr->AnimFlags & ANIM_DISABLED)
604 					|| pSeq->AnimType != PICTURE_ANIM)
605 				continue;
606 
607 			// Draw current animation frame only if changed
608 			if (!fullRedraw && !pSeq->Change)
609 				continue;
610 
611 			s.frame = SetAbsFrameIndex (CommData.AlienFrame,
612 					ADPtr->StartIndex + pSeq->CurIndex);
613 			DrawStamp (&s);
614 			pSeq->Change = FALSE;
615 
616 			Change = TRUE;
617 		}
618 	}
619 
620 	UnbatchGraphics ();
621 
622 	return Change;
623 }
624