1 /*
2  * Kuklomenos
3  * Copyright (C) 2008-2009 Martin Bays <mbays@sdf.lonestar.org>
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see http://www.gnu.org/licenses/.
17  */
18 
19 #include <vector>
20 #include <set>
21 #include <algorithm>
22 #include <cstdio>
23 #include <SDL/SDL.h>
24 #include <SDL_gfxPrimitivesDirty.h>
25 
26 #include "state.h"
27 #include "shot.h"
28 #include "player.h"
29 #include "geom.h"
30 #include "gfx.h"
31 #include "random.h"
32 #include "settings.h"
33 #include "conffile.h"
34 #include "node.h"
35 #include "ai.h"
36 #include "sound.h"
37 
38 const int pentatonicScale[14] = { 0, 2, 5, 7, 9, 12, 14, 17, 19, 21, 24, 26, 29 };
39 const int majorScale[14] = { 0, 2, 4, 5, 7, 9, 11, 12, 14, 16, 17, 19, 21, 23 };
40 const int minorScale[14] = { 0, 2, 3, 5, 7, 8, 10, 12, 14, 15, 19, 20, 22 };
41 const int* scales[3] = { pentatonicScale, majorScale, minorScale };
42 
43 // beatRatios: beat frequency of inner is beatRatio * frequency of outer
44 const float beatRatios[5] = { 1.0/1.0, 3.0/2.0, 4.0/3.0, 5.0/4.0, 5.0/3.0 };
45 
GameState(int speed)46 GameState::GameState(int speed) :
47     targettedNode(NULL), mutilationWave(-1), preMutilationPhase(0),
48     extractPreMutCutoff(350), freeViewMode(false),
49     extracted(0), extractDecayRate(0.0002), you(), zoomdist(0), invaderRate(0),
50     speed(speed), extractMax(500), end(END_NOT), ai(NULL)
51 {
52     setRating();
53     invaderCooldown = invaderRate;
54 
55     std::vector<NodeColour> colours;
56     for (int i = 0; i < 6; i++)
57 	colours.push_back( NodeColour(i) );
58 
59     float baseAngleInner = 0;
60     float baseAngleOuter = 0;
61     int innerDir = 1;
62     if (this->rating > 5.0)
63     {
64 	// Let's mix things up a bit...
65 	colours = randomSort(colours);
66 	baseAngleInner = ranf(4.0);
67 	baseAngleOuter = ranf(4.0);
68 	if (rani(2) == 0)
69 	    innerDir = -1;
70     }
71 
72     const int* scale = scales[rani(3)];
73     const int key = rani(12) - 8;
74     const float beatRatio = beatRatios[rani(5)];
75 
76     set<int> notesUsed;
77 
78     int i=0;
79     for (std::vector<NodeColour>::iterator it = colours.begin();
80 	    it != colours.end();
81 	    i++, it++)
82     {
83 	const NodeColour colour = *it;
84 
85 	int note = rani(14);
86 	while (notesUsed.find(note) != notesUsed.end())
87 	    note = rani(14);
88 	notesUsed.insert(note);
89 
90 	const int pitch = int(1000 *
91 		pow(2, - (key+scale[note]) / 12.0) );
92 	if (i < 3)
93 	    nodes.push_back(
94 		    Node(RelPolarCoord(baseAngleInner+2+i*4.0/3,
95 			    ARENA_RAD * 5/9),
96 			innerDir*1.0, colour,
97 			beatRatio*1.0/3000, rani(16)/4.0, pitch));
98 	else
99 	    nodes.push_back(
100 		    Node(RelPolarCoord(baseAngleOuter+i*4.0/3,
101 			    ARENA_RAD * 2/3),
102 			-innerDir*1.0, colour,
103 			1.0/3000, rani(16)/4.0, pitch));
104     }
105 }
106 
isNullInvp(Invader * p)107 bool isNullInvp(Invader* p)
108 {
109     return (p == NULL);
110 }
111 
youHaveNode(NodeColour colour)112 bool GameState::youHaveNode(NodeColour colour)
113 {
114     for (std::vector<Node>::iterator it = nodes.begin();
115 	    it != nodes.end();
116 	    it++)
117 	if (it->status == NODEST_YOU && it->nodeColour == colour)
118 	    return true;
119     return false;
120 }
youHaveShotNode(int type)121 bool GameState::youHaveShotNode(int type)
122 {
123     switch (type)
124     {
125 	case 0: return youHaveNode(NODEC_GREEN); break;
126 	case 1: return youHaveNode(NODEC_YELLOW); break;
127 	case 2: return youHaveNode(NODEC_RED); break;
128 	case 3: return youHaveNode(NODEC_BLUE); break;
129 	default: return false;
130     }
131 }
evilHasNode(NodeColour colour)132 bool GameState::evilHasNode(NodeColour colour)
133 {
134     for (std::vector<Node>::iterator it = nodes.begin();
135 	    it != nodes.end();
136 	    it++)
137 	if (it->status == NODEST_EVIL && it->nodeColour == colour)
138 	    return true;
139     return false;
140 }
141 
shotHeat(int type)142 int GameState::shotHeat(int type)
143 {
144     if (type == 3)
145 	return you.shootMaxHeat-1;
146 
147     int baseHeat;
148     const bool super = youHaveShotNode(type);
149     switch (type)
150     {
151 	case 0: baseHeat = 4000; break;
152 	case 1: baseHeat = 7000; break;
153 	case 2: baseHeat = 10000; break;
154 	default: baseHeat = 0;
155     }
156     return super ? 7*baseHeat/10 : baseHeat;
157 }
shotDelay(int type)158 int GameState::shotDelay(int type)
159 {
160     int delay;
161     const bool super = youHaveShotNode(type);
162     switch (type)
163     {
164 	case 0: delay = super ? 100 : 200; break;
165 	case 1: delay = super ? 250 : 300; break;
166 	case 2: delay = super ? 300 : 400; break;
167 	case 3: delay = 10000; break;
168 	default: delay = 0;
169     }
170     return delay;
171 }
172 
updateMutilation(int time)173 void GameState::updateMutilation(int time)
174 {
175     if (mutilationWave > 0)
176     {
177 	mutilationWave = std::max(0.0f,
178 		mutilationWave - time*0.001f*ARENA_RAD/3);
179 	if (mutilationWave < 0)
180 	    mutilationWave = 0;
181 
182 	for (std::vector<Node>::iterator it = nodes.begin();
183 		it != nodes.end();
184 		it++)
185 	    if (it->pos.dist >= mutilationWave-it->radius)
186 		if (it->status != NODEST_NONE &&
187 			it->status != NODEST_DESTROYED)
188 		{
189 		    it->status = NODEST_NONE;
190 
191 		    soundEvents.newEvent(it->cpos() - ARENA_CENTRE,
192 			    mutChunk, 128, it->pitch,
193 			    true);
194 		}
195 
196 	for (std::vector<Invader*>::iterator it = invaders.begin();
197 		it != invaders.end();
198 		it++)
199 	    if (((*it)->cpos() - ARENA_CENTRE).lengthsq() >=
200 		    mutilationWave*mutilationWave)
201 	    {
202 		int damage = (*it)->die();
203 		if ((*it)->dead())
204 		{
205 		    soundEvents.newEvent((*it)->cpos() - ARENA_CENTRE,
206 			    invDieChunk,
207 			    128, 500+100*damage + int(200*gaussian()),
208 			    true);
209 		    soundEvents.newEvent((*it)->cpos() - ARENA_CENTRE,
210 			    mutChunk,
211 			    128, 500+100*damage + int(200*gaussian()),
212 			    true);
213 		}
214 	    }
215 
216 	for (std::vector<Shot>::iterator it = shots.begin();
217 		it != shots.end();
218 		it++)
219 	    if ((it->pos - ARENA_CENTRE).lengthsq() >=
220 		    mutilationWave*mutilationWave)
221 	    {
222 		it->die();
223 		deadShots = true;
224 	    }
225 
226 	while (!you.dead && mutilationWave <= you.radius())
227 	{
228 	    soundEvents.newEvent(0, shieldChunk,
229 		    128, 1000 + 100*int(you.shield));
230 	    you.shield -= 1;
231 	    if (you.shield < 0)
232 	    {
233 		you.dead = true;
234 		soundEvents.newEvent(0, mutChunk,
235 			128, 2000);
236 	    }
237 	    else
238 		soundEvents.newEvent(0, mutChunk,
239 			128, 1000 + 100*int(you.shield));
240 	    end = END_EXTRACTED;
241 	}
242     }
243     else if (extracted > extractPreMutCutoff && mutilationWave == -1)
244     {
245 	preMutilationPhase += time*0.002;
246 	if (preMutilationPhase < time*0.002)
247 	{
248 	    float stage = float(extracted - extractPreMutCutoff) /
249 		(extractMax - extractPreMutCutoff );
250 	    soundEvents.newEvent(0, mutChunk,
251 		    int(24 + 48 * stage),
252 		    int(1400 - 400 * stage));
253 	}
254     }
255 }
256 
update(int time,bool noInput)257 void GameState::update(int time, bool noInput)
258 {
259     if (time <= 0)
260 	return;
261 
262     deadShots = false;
263 
264     if (ai)
265 	ai->update(time);
266 
267     updateObjects(time);
268 
269     if (freeViewMode)
270     {
271 	if (!noInput)
272 	    handleFreeViewInput(time);
273     }
274     else
275     {
276 	if ( (!noInput || ai) && !you.dead)
277 	    handleGameInput(time);
278 	updateZoom(time);
279     }
280 
281     evilAI(time);
282 
283     soundEvents.update(RelPolarCoord(you.aim.angle, zoomdist));
284 
285     cleanup();
286 }
287 
updateObjects(int time)288 void GameState::updateObjects(int time)
289 {
290     you.update(time, youHaveNode(NODEC_CYAN));
291 
292     if (!end && you.dead)
293 	end = END_DEAD;
294 
295     extracted -= extractDecayRate*(end == END_WIN ? 10 : 1)*time;
296     extracted = std::max(0.0, extracted);
297 
298     int destroyedCount = 0;
299     for (std::vector<Node>::iterator it = nodes.begin();
300 	    it != nodes.end();
301 	    it++)
302     {
303 	it->update(time);
304 
305 	if (it->status == NODEST_EVIL)
306 	{
307 	    extracted += it->extract(time, evilHasNode(NODEC_CYAN));
308 	    if (extracted > extractMax && mutilationWave == -1)
309 	    // initiate wave of mutilation
310 		mutilationWave = ARENA_RAD;
311 	}
312 	else if (it->status == NODEST_DESTROYED)
313 	    destroyedCount++;
314     }
315     if (destroyedCount >= 4 && end == END_NOT)
316     {
317 	// you win - set invaders fleeing away
318 	end = END_WIN;
319 	for (std::vector<Invader*>::iterator it = invaders.begin();
320 		it != invaders.end();
321 		it++)
322 	    (*it)->fleeOnWin();
323     }
324 
325     updateMutilation(time);
326 
327     std::vector<Invader*> topush;
328 
329     for (std::vector<Invader*>::iterator it = invaders.begin();
330 	    it != invaders.end();
331 	    it++)
332     {
333 	Invader* inv = *it;
334 	inv->update(time);
335 
336 	while (!inv->spawns.empty())
337 	{
338 	    Invader* spawned = inv->spawns.back();
339 	    inv->spawns.pop_back();
340 	    topush.push_back(spawned);
341 	}
342 
343 	if (inv->hitsYou() &&
344 		inv->collObj().circleIntersects(ARENA_CENTRE, you.radius()))
345 	{
346 	    inv->die();
347 	    if (!you.dead)
348 	    {
349 		soundEvents.newEvent(inv->cpos()-ARENA_CENTRE, shieldChunk,
350 			96, 1000 + 100*int(you.shield));
351 		you.shield -= 1;
352 		if (you.shield < 0 && !settings.invuln)
353 		    you.dead = true;
354 	    }
355 	}
356 
357 	// check for leaving arena
358 	if (((inv)->cpos() - ARENA_CENTRE).lengthsq() >=
359 		ARENA_RAD*ARENA_RAD)
360 	    inv->die();
361     }
362 
363     // check for collisions between invaders
364     // (note that this must come after updating all invaders, since we need
365     // the collision objects to be set)
366     for (std::vector<Invader*>::iterator it = invaders.begin();
367 	    it != invaders.end();
368 	    it++)
369     {
370 	Invader* inv = *it;
371 	if (inv->hitsInvaders() > 0)
372 	{
373 	    Invader* hitInvader = NULL;
374 	    const float r = inv->hitsInvaders();
375 	    for (std::vector<Invader*>::iterator it2 = invaders.begin();
376 		    it2 != invaders.end();
377 		    it2++)
378 	    {
379 		if (*it2 == inv)
380 		    continue;
381 		if ((*it2)->collObj().circleIntersects(inv->cpos(), r))
382 		{
383 		    hitInvader = *it2;
384 		    break;
385 		}
386 	    }
387 
388 	    if (hitInvader != NULL)
389 	    {
390 		int damage = hitInvader->die();
391 		if (hitInvader->dead())
392 		{
393 		    soundEvents.newEvent(hitInvader->cpos() - ARENA_CENTRE,
394 			    invDieChunk,
395 			    96, 800+200*damage + int(100*gaussian()));
396 		    you.score += hitInvader->killScore();
397 		}
398 		inv->hit(damage);
399 	    }
400 	}
401     }
402 
403     for (std::vector<Shot>::iterator it = shots.begin();
404 	    it != shots.end();
405 	    it++)
406     {
407 	float hitTime = -1;
408 	Invader* hitInvader = NULL;
409 
410 	for (std::vector<Invader*>::iterator invit = invaders.begin();
411 		invit != invaders.end();
412 		invit++)
413 	{
414 	    if (!(*invit)->hitsShots())
415 		continue;
416 
417 	    RelCartCoord v = it->vel;
418 	    float t = (*invit)->collObj().pointHits(it->pos, v, time);
419 	    if (t >= 0 && (hitTime == -1 || t < hitTime))
420 	    {
421 		hitTime = t;
422 		hitInvader = *invit;
423 	    }
424 	}
425 	for (std::vector<Node>::iterator nodeit = nodes.begin();
426 		nodeit != nodes.end();
427 		nodeit++)
428 	{
429 	    if (nodeit->primed < 1)
430 		continue;
431 
432 	    RelCartCoord v = it->vel;
433 	    float t = nodeit->collObj().pointHits(it->pos, v, time);
434 	    if (t >= 0 && (hitTime == -1 || t < hitTime))
435 	    {
436 		hitTime = t;
437 		hitInvader = &*nodeit;
438 	    }
439 	}
440 
441 	if (hitInvader != NULL)
442 	{
443 	    int damage = hitInvader->hit(it->weight);
444 	    if (hitInvader->dead())
445 	    {
446 		soundEvents.newEvent(it->pos - ARENA_CENTRE, invDieChunk,
447 			96, 800+200*damage + int(100*gaussian()));
448 		you.score += hitInvader->killScore();
449 	    }
450 	    else
451 		soundEvents.newEvent(it->pos - ARENA_CENTRE, invHitChunk,
452 			96, 800+200*damage + int(100*gaussian()));
453 	    it->hit(damage);
454 	    if (Shot::is_dead(*it))
455 		deadShots = true;
456 	}
457 
458 	it->update(time);
459 	RelCartCoord d = it->pos - ARENA_CENTRE;
460 	if (d.lengthsq() >= ARENA_RAD*ARENA_RAD)
461 	{
462 	    it->dead = 1;
463 	    deadShots = true;
464 	}
465     }
466 
467     // add newly spawned invaders
468     for (std::vector<Invader*>::iterator it = topush.begin();
469 	    it != topush.end();
470 	    it++)
471     {
472 	invaders.push_back(*it);
473     }
474 }
475 
handleGameInput(int time)476 void GameState::handleGameInput(int time)
477 {
478     bool keyRotLeft;
479     bool keyRotRight;
480     bool keyDeAim;
481     bool keyDeZoom;
482     bool keyShoot1;
483     bool keyShoot2;
484     bool keyShoot3;
485     bool keyShootPod;
486 
487     if (!ai)
488     {
489 	keyRotLeft = settings.keybindings[C_LEFT].isPressed();
490 	keyRotRight = settings.keybindings[C_RIGHT].isPressed();
491 	keyDeAim = settings.keybindings[C_DEAIM].isPressed();
492 	keyDeZoom = settings.keybindings[C_DEZOOM].isPressed();
493 	keyShoot1 = settings.keybindings[C_SHOOT_GREEN].isPressed();
494 	keyShoot2 = settings.keybindings[C_SHOOT_YELLOW].isPressed();
495 	keyShoot3 = settings.keybindings[C_SHOOT_RED].isPressed();
496 	keyShootPod = settings.keybindings[C_SHOOT_POD].isPressed();
497     }
498     else
499     {
500 	keyRotLeft = ai->keys & ai->K_LEFT;
501 	keyRotRight = ai->keys & ai->K_RIGHT;
502 	keyDeAim = ai->keys & ai->K_DEAIM;
503 	keyDeZoom = ai->keys & ai->K_DEZOOM;
504 	keyShoot1 = ai->keys & ai->K_SHOOT1;
505 	keyShoot2 = ai->keys & ai->K_SHOOT2;
506 	keyShoot3 = ai->keys & ai->K_SHOOT3;
507 	keyShootPod = ai->keys & ai->K_POD;
508     }
509 
510     if (keyRotLeft || keyRotRight)
511     {
512 	if (keyRotLeft)
513 	    you.aim.angle += time*.015*settings.turnRateFactor*you.aimAccuracy();
514 	else
515 	    you.aim.angle += time*-.015*settings.turnRateFactor*you.aimAccuracy();
516 	you.aim.dist = std::max(AIM_MIN, (float)(you.aim.dist-time*.04));
517     }
518     if (keyDeAim)
519 	you.aim.dist = std::max(AIM_MIN, (float)(you.aim.dist-time*.1));
520     else if (keyDeZoom)
521     {
522 	you.aim.dist = std::max(AIM_MIN, (float)(you.aim.dist-time*.04));
523 	zoomdist = std::max(ZOOM_MIN, (float)(zoomdist-time*.2));
524     }
525     if (!(keyDeAim || keyDeZoom || keyRotLeft || keyRotRight))
526     {
527 	// exponential decay from AIM_MIN to AIM_MAX:
528 	// dist = max - (max-min)*2^{-time*rate}
529 	// so d(dist)/d(time) is proportional to (max-dist).
530 	// In fact, we define:
531 	// max - dist = min(max, (max-dist_0) * exp(-time*aimRate))
532 	const float aimRate = youHaveNode(NODEC_PURPLE) ? 0.0006 : 0.0004;
533 	you.aim.dist = std::min(AIM_MAX,
534 		(float)(you.aim.dist + (AIM_MAX-you.aim.dist)*time*aimRate));
535     }
536 
537     float mintheta = 4;
538     for (std::vector<Node>::iterator it = nodes.begin();
539 	    it != nodes.end();
540 	    it++)
541     {
542 	Angle relAngle = you.aim.angle - it->pos.angle;
543 	const float dtheta = std::min(float(relAngle), 4.0f-relAngle);
544 	if (it->status != NODEST_DESTROYED &&
545 		dtheta < mintheta)
546 	{
547 	    mintheta = dtheta;
548 	    targettedNode = &*it;
549 	}
550     }
551     if (mintheta > 0.5)
552 	targettedNode = NULL;
553 
554     you.shootTimer -= time;
555     you.podTimer -= time;
556 
557     if ((keyShoot1 || keyShoot2 || keyShoot3)
558 	    && you.shootTimer <= 0)
559     {
560 	int weight=0;
561 	bool super;
562 	if (keyShoot3 &&
563 		you.shootHeat < you.shootMaxHeat - shotHeat(2))
564 	{
565 	    weight = 3;
566 	    super = youHaveNode(NODEC_RED);
567 	}
568 	else if (keyShoot2 &&
569 		you.shootHeat < you.shootMaxHeat - shotHeat(1))
570 	{
571 	    weight = 2;
572 	    super = youHaveNode(NODEC_YELLOW);
573 	}
574 	else if (keyShoot1 &&
575 		you.shootHeat < you.shootMaxHeat - shotHeat(0))
576 	{
577 	    weight = 1;
578 	    super = youHaveNode(NODEC_GREEN);
579 	}
580 	if (weight > 0)
581 	{
582 	    float noise = gaussian()*you.aimAccuracy();
583 
584 	    Shot shot( ARENA_CENTRE,
585 		    RelPolarCoord(you.aim.angle+noise,
586 			0.1+0.05*(3-weight) + super*0.05),
587 		    weight, super);
588 	    // we're generous, and assume that the command to shoot was given
589 	    // just after the last update, and the shot was fired as soon as
590 	    // the heat became low enough:
591 	    shot.update(std::min(time,
592 			std::min(
593 			    (you.shootMaxHeat - you.shootHeat -
594 			     shotHeat(weight-1))/you.shootCoolrate,
595 			    -you.shootTimer)));
596 
597 	    shots.push_back(shot);
598 
599 	    const int pitch = 900+weight*100 + int(40*gaussian());
600 	    soundEvents.newEvent(shot.pos-ARENA_CENTRE, shotChunk,
601 		    32, pitch);
602 	    if (super)
603 		soundEvents.newEvent(shot.pos-ARENA_CENTRE, shotChunk,
604 			32, pitch*3/4);
605 
606 	    you.shootHeat += shotHeat(weight-1);
607 	    you.shootTimer = shotDelay(weight-1);
608 
609 	    //you.aim.angle += weight*noise/4;
610 
611 	    you.aim.dist = std::max(AIM_MIN,
612 		    (float)(you.aim.dist-weight*6));
613 	}
614     }
615     if (keyShootPod &&
616 	    targettedNode != NULL &&
617 	    you.podTimer <= 0 &&
618 	    you.shootHeat < you.shootMaxHeat - shotHeat(3))
619     {
620 	const bool super = youHaveNode(NODEC_BLUE);
621 	invaders.push_back(new CapturePod(targettedNode,
622 		    RelPolarCoord(you.aim.angle, 5),
623 		    super));
624 	you.podTimer = shotDelay(3);
625 	you.shootHeat += shotHeat(3);
626 	you.doneLaunchedPod = true;
627 
628 	const int pitch = 1500 + int(40*gaussian());
629 	soundEvents.newEvent(0, shotChunk,
630 		32, pitch);
631 	if (super)
632 	    soundEvents.newEvent(0, shotChunk,
633 		    32, pitch*3/4);
634     }
635 
636     you.shootTimer = std::max(0, you.shootTimer);
637     you.podTimer = std::max(0, you.podTimer);
638 }
639 
handleFreeViewInput(int time)640 void GameState::handleFreeViewInput(int time)
641 {
642     // "free view mode": not useful or anything, but kind of fun
643 
644     static bool dirKeys[4];
645     dirKeys[0] = settings.keybindings[C_DEZOOM].isPressed() ||
646 	settings.keybindings[C_M_UP].isPressed();
647     dirKeys[1] = settings.keybindings[C_LEFT].isPressed() ||
648 	settings.keybindings[C_M_LEFT].isPressed();
649     dirKeys[2] = settings.keybindings[C_DEAIM].isPressed() ||
650 	settings.keybindings[C_M_DOWN].isPressed();
651     dirKeys[3] = settings.keybindings[C_RIGHT].isPressed() ||
652 	settings.keybindings[C_M_RIGHT].isPressed();
653     bool keyZoomIn = settings.keybindings[C_SHOOT_GREEN].isPressed();
654     bool keyZoomOut = settings.keybindings[C_SHOOT_YELLOW].isPressed();
655     bool keyRotLeft = settings.keybindings[C_SHOOT_RED].isPressed();
656     bool keyRotRight = settings.keybindings[C_SHOOT_POD].isPressed();
657 
658     const float baseZoom = (float)screenGeom.rad / (float)ARENA_RAD;
659     const float maxZoom = baseZoom * 32;
660     const float minZoom = baseZoom;
661     bool moved = false;
662     for (int i = 0; i < 4; i++)
663 	if (dirKeys[i])
664 	{
665 	    moved = true;
666 	    freeView.centre += RelPolarCoord(i-freeView.angle,
667 		    0.3*time*baseZoom/freeView.zoom);
668 	}
669 
670     // half a second of zooming doubles/halves the zoom
671     if (keyZoomOut)
672 	freeView.zoom -= freeView.zoom * time * 0.0014;
673     if (keyZoomIn)
674 	freeView.zoom += freeView.zoom * time * 0.0014;
675 
676     if (keyRotLeft)
677 	freeView.angle += 0.00075*time;
678     if (keyRotRight)
679 	freeView.angle -= 0.00075*time;
680 
681     RelPolarCoord freeViewCentre = freeView.centre - ARENA_CENTRE;
682     if (moved)
683 	freeView.zoom = std::max(freeView.zoom,
684 		(float)screenGeom.rad / ((float)ARENA_RAD - freeViewCentre.dist));
685     freeView.zoom = std::min(maxZoom, std::max(minZoom, freeView.zoom));
686     freeViewCentre.dist = std::min(freeViewCentre.dist,
687 	    (float)ARENA_RAD - ((float)screenGeom.rad / freeView.zoom));
688     freeView.centre = ARENA_CENTRE + freeViewCentre;
689 }
690 
updateZoom(int time)691 void GameState::updateZoom(int time)
692 {
693     float targetZoomdist = you.aim.dist;
694     if (targetZoomdist < 0)
695 	targetZoomdist = 0;
696     if (targetZoomdist > ZOOMDIST_MAX)
697 	targetZoomdist = ZOOMDIST_MAX;
698     if (end)
699 	targetZoomdist = 0;
700     if (time >= 200)
701 	zoomdist = targetZoomdist;
702     else
703 	zoomdist += (targetZoomdist-zoomdist)*time/200;
704 
705     if (end == END_DEAD || end == END_EXTRACTED && zoomdist < 1)
706     {
707 	freeViewMode = true;
708 	freeView = View(ARENA_CENTRE,
709 		(float)screenGeom.rad / (float)ARENA_RAD, -you.aim.angle);
710     }
711 }
712 
evilAI(int time)713 void GameState::evilAI(int time)
714 {
715     invaderCooldown -= time;
716     if (end != END_WIN &&
717 	    invaderCooldown <= 0 &&
718 	    rani(invaderRate/3) <= time)
719     {
720 	// it's about time to spawn a new invader
721 	Invader* p_inv = NULL;
722 	int cost = 0;
723 	while (cost == 0)
724 	{
725 	    //int type = rani(5) == 0 ? 3 : rani(3);
726 	    int type = rani(4);
727 	    if (mutilationWave > -1)
728 		type = 3;
729 	    RelPolarCoord pos;
730 	    int ds;
731 	    switch (type)
732 	    {
733 		case 0: case 1: case 2:
734 		    {
735 			bool super = evilHasNode(
736 				type == 0 ? NODEC_RED :
737 				type == 1 ? NODEC_YELLOW :
738 				NODEC_GREEN);
739 			cost = 1;
740 			pos = RelPolarCoord(ranf()*4, ARENA_RAD-20);
741 			ds = super ? rani(9)-4 : rani(5)-2;
742 			switch (type)
743 			{
744 			    case 0: p_inv = new EggInvader(pos, ds, super);
745 				    break;
746 			    case 1: p_inv = new KamikazeInvader(pos, ds,
747 					    super); break;
748 			    case 2: p_inv = new SplittingInvader(pos, ds,
749 					    super); break;
750 			}
751 			break;
752 		    }
753 		case 3:
754 		    {
755 			// blue meanies cost 3 times as much to Evil as
756 			// normal invaders. In order to keep the average rate
757 			// of normal invaders constant, this cost is charged
758 			// even if, due to lack of targets, no meanie is
759 			// actually spawned.
760 			cost = 3;
761 
762 			std::vector<Node*> possibleTargets;
763 			for (std::vector<Node>::iterator it = nodes.begin();
764 				it != nodes.end();
765 				it++)
766 			{
767 			    if ((it->status == NODEST_NONE ||
768 					it->status == NODEST_YOU) &&
769 				    it->targettingInfester == NULL)
770 				possibleTargets.push_back(&*it);
771 			}
772 			if (possibleTargets.size() > 0)
773 			{
774 			    const int i = rani(possibleTargets.size());
775 			    p_inv = new InfestingInvader(possibleTargets[i],
776 				    evilHasNode(NODEC_BLUE));
777 			}
778 		    }
779 		    break;
780 		case 4:
781 		    // FoulEggLayingInvader - unused
782 		    cost = 1;
783 		    pos = RelPolarCoord(ranf()*4, ARENA_RAD-(20+rani(5)*10));
784 		    ds = rani(5)-2;
785 		    p_inv = new FoulEggLayingInvader(pos, ds);
786 		    break;
787 	    }
788 	}
789 	if (p_inv)
790 	    invaders.push_back(p_inv);
791 	invaderCooldown += invaderRate * cost;
792     }
793 
794     if (evilHasNode(NODEC_PURPLE) &&
795 	    rani(7500) < time)
796     {
797 	for (std::vector<Invader*>::iterator it = invaders.begin();
798 		it != invaders.end();
799 		it++)
800 	    (*it)->dodge();
801     }
802 }
803 
cleanup()804 void GameState::cleanup()
805 {
806     if (deadShots)
807     {
808 	shots.erase(remove_if(shots.begin(),
809 		    shots.end(), Shot::is_dead),
810 		shots.end());
811     }
812 
813     for (std::vector<Invader*>::iterator it = invaders.begin();
814 	    it != invaders.end();
815 	    it++)
816     {
817 	if ((*it)->dead())
818 	{
819 	    (*it)->onDeath();
820 	    delete *it;
821 	    (*it) = NULL;
822 	}
823     }
824     invaders.erase(remove_if(invaders.begin(),
825 		invaders.end(), isNullInvp),
826 	    invaders.end());
827 }
828 
draw(SDL_Surface * surface)829 void GameState::draw(SDL_Surface* surface)
830 {
831     View view;
832     View boundView;
833 
834     if (!freeViewMode)
835     {
836 	const RelPolarCoord d(you.aim.angle, zoomdist);
837 
838 	const View zoomView(ARENA_CENTRE + d,
839 		(float)screenGeom.rad/((float)ARENA_RAD-zoomdist),
840 		settings.rotatingView ? -d.angle : 0);
841 
842 	const View outerView(ARENA_CENTRE,
843 		(float)screenGeom.rad/(float)ARENA_RAD,
844 		settings.rotatingView ? -d.angle : 0);
845 
846 	view = settings.zoomEnabled ? zoomView : outerView;
847 	boundView = zoomView;
848 
849 	if (settings.zoomEnabled)
850 	{
851 	    ((settings.useAA == AA_FORCE) ? aacircleColor : circleColor)
852 		(surface, screenGeom.centre.x, screenGeom.centre.y,
853 		 screenGeom.rad, 0x505050ff);
854 	}
855 	else
856 	{
857 	    Circle(zoomView.centre, ARENA_RAD-zoomdist,
858 		    0x505050ff).draw(surface, view, NULL, true);
859 	}
860     }
861     else
862     {
863 	boundView = view = freeView;
864 	boundView.zoom /= 3;
865 
866 	((settings.useAA == AA_FORCE) ? aacircleColor : circleColor)
867 	    (surface, screenGeom.centre.x, screenGeom.centre.y,
868 	     screenGeom.rad, 0x505050ff);
869     }
870 
871     drawIndicators(surface, view);
872     drawGrid(surface, view);
873     drawTargettingLines(surface, view);
874     drawObjects(surface, view, &boundView);
875     drawNodeTargetting(surface, view);
876 }
877 
drawGrid(SDL_Surface * surface,const View & view)878 void GameState::drawGrid(SDL_Surface* surface, const View& view)
879 {
880     Circle(ARENA_CENTRE, ARENA_RAD,
881 	    0x808080ff).draw(surface, view, NULL, true);
882 
883     if (settings.showGrid)
884     {
885 	for (int i=1; i<6; i++)
886 	    Circle(ARENA_CENTRE, i*ARENA_RAD/6,
887 		    0x30303000 + (i%2==0)*0x08080800 + 0xff
888 		  ).draw(surface, view, NULL, true);
889 	for (int i=0; i<12; i++)
890 	    Line(ARENA_CENTRE, ARENA_CENTRE +
891 		    RelPolarCoord(i*4.0/12, ARENA_RAD),
892 		    0x30303000 + (i%2==0)*0x08080800 + 0xff
893 		).draw(surface, view, NULL, true);
894     }
895 }
896 
drawTargettingLines(SDL_Surface * surface,const View & view)897 void GameState::drawTargettingLines(SDL_Surface* surface, const View& view)
898 {
899     if (!you.dead)
900     {
901 	const Uint32 aimColour =
902 	    (ai) ? (youHaveNode(NODEC_PURPLE) ? 0x00010100 : 0x00010000) :
903 	    youHaveNode(NODEC_PURPLE) ? 0x01000100 : 0x01000000;
904 
905 	for (int dir = -1; dir < 3; dir+=2)
906 	    Line(ARENA_CENTRE, ARENA_CENTRE +
907 		    RelPolarCoord(you.aim.angle + dir*you.aimAccuracy(),
908 			ARENA_RAD),
909 		    aimColour * 0x80 + 0xff).draw(surface, view, NULL, true);
910 
911 	if (fabsf(you.aimAccuracy()) <= .45)
912 	    for (int dir = -1; dir < 3; dir+=2)
913 		Line(ARENA_CENTRE, ARENA_CENTRE +
914 			RelPolarCoord(you.aim.angle + dir*2*you.aimAccuracy(),
915 			    ARENA_RAD),
916 			aimColour * 0x50 + 0xff).draw(surface, view, NULL, true);
917     }
918 }
919 
drawNodeTargetting(SDL_Surface * surface,const View & view)920 void GameState::drawNodeTargetting(SDL_Surface* surface, const View& view)
921 {
922     if (!you.dead && targettedNode != NULL)
923     {
924 	const Uint32 c = ( (you.shootHeat < you.shootMaxHeat - shotHeat(3)) ?
925 		0xff000000 : 0xd0600000 ) + ( (you.podTimer <= 0) ?
926 		    0xff : 0x60);
927 	const CartCoord p = targettedNode->cpos();
928 	const float r = targettedNode->radius;
929 	Line(p + RelCartCoord(-9*r/5, 0), p + RelCartCoord(-7*r/5, 0),
930 		c).draw(surface, view, NULL);
931 	Line(p + RelCartCoord(9*r/5, 0), p + RelCartCoord(7*r/5, 0),
932 		c).draw(surface, view, NULL);
933 	Line(p + RelCartCoord(0, -9*r/5), p + RelCartCoord(0, -7*r/5),
934 		c).draw(surface, view, NULL);
935 	Line(p + RelCartCoord(0, 9*r/5), p + RelCartCoord(0, 7*r/5),
936 		c).draw(surface, view, NULL);
937     }
938 }
939 
drawObjects(SDL_Surface * surface,const View & view,View * boundView)940 void GameState::drawObjects(SDL_Surface* surface, const View& view,
941 	View* boundView)
942 {
943     you.draw(surface, view, NULL);
944 
945     for (std::vector<Shot>::iterator it = shots.begin();
946 	    it != shots.end();
947 	    it++)
948 	it->draw(surface, view, boundView);
949 
950     for (std::vector<Invader*>::iterator it = invaders.begin();
951 	    it != invaders.end();
952 	    it++)
953 	(*it)->draw(surface, view, boundView);
954 
955     for (std::vector<Node>::iterator it = nodes.begin();
956 	    it != nodes.end();
957 	    it++)
958 	it->draw(surface, view, boundView);
959 
960     if (mutilationWave > 0)
961 	Circle(ARENA_CENTRE, mutilationWave, 0x00ffffff).draw(surface, view, NULL);
962     else if (extracted > extractPreMutCutoff && extracted < extractMax)
963 	Circle(ARENA_CENTRE, ARENA_RAD,
964 		0x00ffff00 + (0x4f + 0x80*(
965 			((int)extracted - extractPreMutCutoff) /
966 			(extractMax - extractPreMutCutoff)) +
967 		    (int)(0x30 * preMutilationPhase.sinf()))
968 		).draw(surface, view, NULL);
969 }
970 
971 // approxAtan2Frac: approximates atan2(y,x)*(6/PI)-1
972 //  (being the linear function of atan2(y,x) which is 0 at PI/6 and 1 at PI/3)
approxAtan2Frac(int y,int x)973 float approxAtan2Frac(int y, int x)
974 {
975     static const int N = 10;
976     static const float left=0.5, right=2.0;
977     static float z0[N];
978     static float t0[N], t1[N];
979     static const float halfDist = (right-left)/(2*(N-1));
980     static bool preCalced=false;
981 
982     if (!preCalced)
983     {
984 	// calculate first two terms of the Taylor expansion around some
985 	// values of z spaced uniformly along the interval
986 	for (int i = 0; i < N; i++)
987 	{
988 	    z0[i] = left + (right-left)*i/(N-1);
989 
990 	    t0[i] = atan(z0[i])*(6.0/PI)-1;
991 	    t1[i] = (1.0/(1+z0[i]*z0[i]))*(6.0/PI);
992 	}
993 	preCalced = true;
994     }
995 
996     float z = float(y)/x;
997 
998     // use the precalculated linear approximation around the closest z value
999     for (int i = 0; i < N; i++)
1000 	if ( i == N-1 || z < z0[i] + halfDist )
1001 	    return t0[i] + t1[i]*(z-z0[i]);
1002 
1003     return 0; // won't happen
1004 
1005     // cubic approximation to atan(z) around z=1:
1006     // return PI/4 + (z-1)/2 - (z-1)*(z-1)/4 + (z-1)*(z-1)*(z-1)/12;
1007 }
1008 
drawIndicators(SDL_Surface * surface,const View & view)1009 void GameState::drawIndicators(SDL_Surface* surface, const View& view)
1010 {
1011     // heat, shield and extraction indicators:
1012     Uint32 colour;
1013     for (int x = screenGeom.rad/2;
1014 	    x <= 866*screenGeom.rad/1000 + 15;
1015 	    x++)
1016     {
1017 	const int xsq = x*x;
1018 	int rsq;
1019 	for (int y = int(sqrt(screenGeom.indicatorRsqLim1 - xsq));
1020 		(rsq = xsq + y*y) <= screenGeom.indicatorRsqLim4;
1021 		y++)
1022 	{
1023 	    if (rsq < screenGeom.indicatorRsqLim1)
1024 		continue;
1025 	    const float frac = approxAtan2Frac(y,x);
1026 	    if (frac >= 0 && frac <= 1)
1027 	    {
1028 		if (rsq <= screenGeom.indicatorRsqLim2)
1029 		{
1030 		    // decay towards the edges, for prettiness
1031 		    const int decay = std::min(255,
1032 			    std::min(rsq - screenGeom.indicatorRsqLim1,
1033 				screenGeom.indicatorRsqLim2 - rsq)/2);
1034 		    int intensity;
1035 
1036 		    // heat
1037 		    intensity =
1038 			you.shootHeat > you.shootMaxHeat*frac ? 55+int(200*frac) :
1039 			(frac >= 0.98 ? int(5000*(frac-0.98)) : 0) + (
1040 				35 );
1041 		    colour = 0x01000000 * intensity + decay;
1042 
1043 		    pixelColor(surface, screenGeom.centre.x - x,
1044 			    screenGeom.centre.y - y, colour);
1045 
1046 		    // extraction
1047 		    if (extracted <= extractPreMutCutoff)
1048 			intensity =
1049 			    extracted > extractPreMutCutoff*frac ? 55+int(200*frac) :
1050 			    (frac >= 0.98 ? int(5000*(frac-0.98)) : 0) + (
1051 				    evilHasNode(NODEC_CYAN) ? 55 : 35 );
1052 		    else
1053 			intensity =
1054 			    ((extracted - extractPreMutCutoff) >
1055 			     (extractMax - extractPreMutCutoff)*frac) ?
1056 			    85+int(170*frac) :
1057 			    (frac >= 0.98 ? int(5000*(frac-0.98)) : 0) + (
1058 				    evilHasNode(NODEC_CYAN) ? 85 : 60 );
1059 		    colour = 0x00010100 * intensity + decay;
1060 
1061 		    pixelColor(surface, screenGeom.centre.x + x,
1062 			    screenGeom.centre.y - y, colour);
1063 		}
1064 		else if (rsq < screenGeom.indicatorRsqLim3)
1065 		{
1066 		    // shade between heat and shield indicators
1067 		    pixelColor(surface, screenGeom.centre.x - x,
1068 			    screenGeom.centre.y - y, 0xa0);
1069 		}
1070 		else
1071 		{
1072 		    const int decay = std::min(255,
1073 			    std::min(rsq - screenGeom.indicatorRsqLim3,
1074 				screenGeom.indicatorRsqLim4 - rsq)/2);
1075 		    int intensity;
1076 
1077 		    // shield
1078 		    const int i = int(frac*4);
1079 		    const Uint32 baseColour =
1080 			(i == 0) ? 0x01000000 :
1081 			(i == 1) ? 0x01010000 :
1082 			(i == 2) ? 0x00010000 :
1083 			0x00010100;
1084 
1085 		    intensity =
1086 			(you.shield > frac*4) ? 55+(int(4*200*frac))%200 :
1087 			youHaveNode(NODEC_CYAN) ? 55 :
1088 			35;
1089 
1090 		    colour =
1091 			baseColour * intensity + decay;
1092 
1093 		    pixelColor(surface, screenGeom.centre.x - x,
1094 			    screenGeom.centre.y - y, colour);
1095 		}
1096 	    }
1097 	}
1098     }
1099 
1100     // shot indicators
1101     static const double shotIndicatorCos = cos(PI/6-PI/120);
1102     static const double shotIndicatorSin = sin(PI/6-PI/120);
1103     for (int i = 0; i < 4; i++)
1104     {
1105 	const float d = screenGeom.rad + 3 + 5*i;
1106 	const float x = shotIndicatorCos * d;
1107 	const float y = shotIndicatorSin * d;
1108 	const View shotView(CartCoord(x,-y), 1, 0);
1109 	const bool super = youHaveShotNode(i);
1110 
1111 
1112 	if (you.shootHeat >= you.shootMaxHeat - shotHeat(i))
1113 	    continue;
1114 
1115 	if (i == 3)
1116 	{
1117 	    // capture pod
1118 	    if (you.podTimer <= 0)
1119 		CapturePod(NULL, RelPolarCoord(0,0), super).draw(surface, shotView, NULL);
1120 	}
1121 	else
1122 	{
1123 	    const int weight = i+1;
1124 		Shot( CartCoord(0,0),
1125 			RelPolarCoord(-0.3,
1126 			    0.1+0.05*(3-weight) + super*0.04),
1127 			weight, super).draw(surface, shotView, NULL);
1128 	}
1129     }
1130 
1131     // node possession indicators
1132     static const double nodeIndicatorCos[6] = {
1133 	cos(PI/6), cos(PI/6 + PI/36), cos(PI/6 + 2*PI/36),
1134 	cos(PI/6 + 3*PI/36), cos(PI/6 + 4*PI/36), cos(PI/6 + 5*PI/36) };
1135     static const double nodeIndicatorSin[6] = {
1136 	sin(PI/6), sin(PI/6 + PI/36), sin(PI/6 + 2*PI/36),
1137 	sin(PI/6 + 3*PI/36), sin(PI/6 + 4*PI/36), sin(PI/6 + 5*PI/36) };
1138     const float dist = screenGeom.rad + 3 + 20 + 10;
1139 
1140     int youNodeCount = 0;
1141     int evilNodeCount = 0;
1142     for (std::vector<Node>::iterator it = nodes.begin();
1143 	    it != nodes.end();
1144 	    it++)
1145     {
1146 	if (it->status == NODEST_YOU)
1147 	{
1148 	    const View nodeView(CartCoord(
1149 			nodeIndicatorCos[youNodeCount] * dist,
1150 			- nodeIndicatorSin[youNodeCount] * dist),
1151 		    1, 0);
1152 	    Node(RelPolarCoord(0,0), 0, it->nodeColour).draw(surface, nodeView, NULL);
1153 	    youNodeCount++;
1154 	}
1155 	else if (it->status == NODEST_EVIL)
1156 	{
1157 	    const View nodeView(CartCoord(
1158 			- nodeIndicatorCos[evilNodeCount] * dist,
1159 			- nodeIndicatorSin[evilNodeCount] * dist),
1160 		    1, 0);
1161 	    Node(RelPolarCoord(0,0), 0, it->nodeColour).draw(surface, nodeView, NULL);
1162 	    evilNodeCount++;
1163 	}
1164     }
1165 }
1166 
rateOfRating(int rating)1167 int GameState::rateOfRating(int rating)
1168 {
1169     static int rates[] = {
1170 	4000, 3200, 2500, 2250, 2000, 1750, 1500, 1300, 1150};
1171     if (rating < 1)
1172 	return -1;
1173     if (rating > 9)
1174 	// Elite+, Elite++, ...
1175 	return (int)(1000.0*pow(0.9, (double)(rating-9)));
1176     return rates[rating-1];
1177 }
1178 
setRating()1179 void GameState::setRating()
1180 {
1181     double newRating = settings.requestedRating > 0 ?
1182 	settings.requestedRating : config.rating[speed];
1183     if (newRating < 1.0)
1184 	newRating = 1.0;
1185 
1186     rating = newRating;
1187 
1188     double effRating = std::max(1.0, rating - 0.5*speed);
1189 
1190     int i = (int) effRating;
1191     int r1 = rateOfRating(i);
1192     int r2 = rateOfRating(i+1);
1193 
1194     invaderRate = (r1 + (int)((effRating-i)*(r2-r1)));
1195 }
1196 
ratingString(int rating)1197 const char* ratingString(int rating)
1198 {
1199     // Recognise these?
1200     switch (rating)
1201     {
1202 	case 1: return "Harmless";
1203 	case 2: return "Mostly Harmless";
1204 	case 3: return "Poor";
1205 	case 4: return "Average";
1206 	case 5: return "Above Average";
1207 	case 6: return "Competent";
1208 	case 7: return "Dangerous";
1209 	case 8: return "Deadly";
1210 	case 9: return "Elite";
1211 	case 10: return "Elite+";
1212 	case 11: return "Elite++";
1213 	case 12: return "Elite+++";
1214 	default: return "Unrated";
1215     };
1216 }
1217 
getHint()1218 const char* GameState::getHint()
1219 {
1220     if (rating > 5.0 || settings.invuln)
1221 	return "";
1222 
1223     if (end != END_WIN)
1224     {
1225 	// Let's see if we can work out what they need to know...
1226 	if (you.score == 0)
1227 	    return "Hint: use keys '1', '2', '3' to shoot.";
1228 
1229 	if (you.score >= 10)
1230 	{
1231 	    if (!you.doneLaunchedPod)
1232 		return "Hint: use key '4' to capture Nodes.";
1233 
1234 	    bool destroyed = false;
1235 	    for (std::vector<Node>::iterator it = nodes.begin();
1236 		    it != nodes.end();
1237 		    it++)
1238 		if (it->status == NODEST_DESTROYED)
1239 		{
1240 		    destroyed = true;
1241 		    break;
1242 		}
1243 
1244 	    if (!destroyed && (END_EXTRACTED || you.score >= 30))
1245 		return "Hint: hit filled nodes with '3'; destroy 4 to win!";
1246 	}
1247     }
1248 
1249     // Either they won, or they lost for no obvious reason. Maybe we should
1250     // tell them about some things they might not have thought of...
1251     switch (int(rating*10)%6)
1252     {
1253 	case 0:
1254 	    return "Hint: Nodes help whoever has them.";
1255 	case 1:
1256 	    return "Hint: Your shield charges if you stop shooting.";
1257 	case 2:
1258 	    return "Hint: Use Down to turn fast, Up to glance back.";
1259 	case 3:
1260 	    return "Hint: Pods ('4') can kill even shielded blue Blobs.";
1261 	case 4:
1262 	    return "Hint: Green shots won't hurt blue Blobs.";
1263 	case 5:
1264 	    return "Hint: Don't let Evil fill the cyan bar.";
1265 
1266 	default:
1267 	    return "Hint: THIS IS A BUG.";
1268     }
1269 }
1270