Pac-Man Evolution
Loading...
Searching...
No Matches
ObjectsGhost.cpp
1/*----------------------------------------------------------------------
2Pac-Man Evolution - Roberto Prieto
3Copyright (C) 2018-2024 MegaStorm Systems
4contact@megastormsystems.com - http://www.megastormsystems.com
5
6This software is provided 'as-is', without any express or implied
7warranty. In no event will the authors be held liable for any damages
8arising from the use of this software.
9
10Permission is granted to anyone to use this software for any purpose,
11including commercial applications, and to alter it and redistribute it
12freely, subject to the following restrictions:
13
141. The origin of this software must not be misrepresented; you must not
15claim that you wrote the original software. If you use this software
16in a product, an acknowledgment in the product documentation would be
17appreciated but is not required.
182. Altered source versions must be plainly marked as such, and must not be
19misrepresented as being the original software.
203. This notice may not be removed or altered from any source distribution.
21
22------------------------------------------------------------------------
23
24Ghost class
25
26------------------------------------------------------------------------ */
27
28#include "ObjectsGhost.h"
29#include "GameField.h"
30#include "ResourceManager.h"
31#include "MapSearchAStar.h"
32#include "BrainsFactory.h"
33
34// Ghost constructor.
35Ghost::Ghost(Sint32 iObjID, Sint32 iMX, Sint32 iMY, GameField* GF) : Actor(iObjID, iMX, iMY, GF)
36{
37 // Custom attributes for each ghost
38 if(iObjID == PME_OBJECT_GHOST_RED)
39 {
40 iSpeed = PME_GHOST_START_SPEED;
41 iGoingOutTrigger = 0;
42 pointScatteringTarget.iX = 25;
43 pointScatteringTarget.iY = -3;
44 }
45 else if(iObjID == PME_OBJECT_GHOST_PINK)
46 {
47 iSpeed = PME_GHOST_START_SPEED;
48 iGoingOutTrigger = 10;
49 pointScatteringTarget.iX = 2;
50 pointScatteringTarget.iY = -3;
51 }
52 else if(iObjID == PME_OBJECT_GHOST_BLUE)
53 {
54 iSpeed = PME_GHOST_START_SPEED;
55 iGoingOutTrigger = 20;
56 pointScatteringTarget.iX = 25;
57 pointScatteringTarget.iY = 32;
58 }
59 else if(iObjID == PME_OBJECT_GHOST_ORANGE)
60 {
61 iSpeed = PME_GHOST_START_SPEED;
62 iGoingOutTrigger = 30;
63 pointScatteringTarget.iX = 0;
64 pointScatteringTarget.iY = 32;
65 }
66 else
67 {
68 Main::Instance().ILogMgr().get()->msg(LML_CRITICAL, " [Ghost] Ghost ID '%d' unknown, unexpected results!\n", iID);
69 iGoingOutTrigger = -1;
70 }
71
72 // Create our state objects
73 pStateInit = new(std::nothrow) GhostStateInit("Init");
74 pStateEvading = new(std::nothrow) GhostStateEvading("Evading");
75 pStateChasing = new(std::nothrow) GhostStateChasing("Chasing");
76 pStateScattering = new(std::nothrow) GhostStateScattering("Scattering");
77 pStateDeath = new(std::nothrow) GhostStateDeath("Death");
78
79 #ifdef DEBUG_INTERNAL
80 Main::Instance().ILogMgr().get()->msg(LML_LOW, " [Ghost] Ghost ID '%d' created.\n", iID);
81 #endif
82}
83
84// Ghost destructor.
85Ghost::~Ghost()
86{
87 delete pStateInit;
88 pStateInit = nullptr;
89 delete pStateEvading;
90 pStateEvading = nullptr;
91 delete pStateChasing;
92 pStateChasing = nullptr;
93 delete pStateScattering;
94 pStateScattering = nullptr;
95 delete pStateDeath;
96 pStateDeath = nullptr;
97 #ifdef DEBUG_INTERNAL
98 Main::Instance().ILogMgr().get()->msg(LML_LOW, " [Ghost] Ghost ID '%d' deleted.\n", iID);
99 #endif
100}
101
102// Ghost logic execution.
103// Return PME_LOOP
104Sint32 Ghost::execute()
105{
106 // Execute current state.
107 // Note the states have full access to all our methods/attributes
108 if(pCurrentState) pCurrentState->execute(this);
109
110 // Call base method (set the object sprite position)
111 Actor::execute();
112
113 return PME_LOOP;
114}
115
116// Debug Ghost object.
117#define PME_DEBUG_NUM_GHOST_ATTRIBUTES 6
118Sint32 Ghost::debug(Sint32 iMode)
119{
120 Main &mC64 = Main::Instance();
121 string sDebug, sTmp;
122 Font* pFont = Main::Instance().IFontMgr().get(ResourceManager::Instance().get(RM_FONT_CONSOLE));
123
124 // Call our base class (render target sprite)
125 Actor::debug(iMode);
126
127 // Start coordinates for each ghost
128 Sint32 iDebugStartY = 0;
129 switch(iID)
130 {
131 case PME_OBJECT_GHOST_RED:
132 iDebugStartY = PME_DEBUG_PANEL_GHOST_Y + iMazeRenderingY;
133 break;
134 case PME_OBJECT_GHOST_BLUE:
135 iDebugStartY = PME_DEBUG_PANEL_GHOST_Y + iMazeRenderingY + 1 * ((PME_DEBUG_NUM_GHOST_ATTRIBUTES * 16) + 16);
136 break;
137 case PME_OBJECT_GHOST_PINK:
138 iDebugStartY = PME_DEBUG_PANEL_GHOST_Y + iMazeRenderingY + 2 * ((PME_DEBUG_NUM_GHOST_ATTRIBUTES * 16) + 16);
139 break;
140 case PME_OBJECT_GHOST_ORANGE:
141 iDebugStartY = PME_DEBUG_PANEL_GHOST_Y + iMazeRenderingY + 3 * ((PME_DEBUG_NUM_GHOST_ATTRIBUTES * 16) + 16);
142 break;
143 }
144
145 // Ghost information
146 pFont->setPosition(PME_DEBUG_PANEL_GHOST_X, iDebugStartY);
147 getName(sDebug);
148 sDebug += " info";
149 pFont->render(sDebug);
150
151 // Maze and pixel position
152 mC64.ITool().intToStrDec(iMazeX, sTmp);
153 sDebug = "Position: [";
154 sDebug += sTmp;
155 sDebug += ",";
156 mC64.ITool().intToStrDec(iMazeY, sTmp);
157 sDebug += sTmp;
158 sDebug += "] (";
159 mC64.ITool().intToStrDec(iPositionX, sTmp);
160 sDebug += sTmp;
161 sDebug += ",";
162 mC64.ITool().intToStrDec(iPositionY, sTmp);
163 sDebug += sTmp;
164 sDebug += ")";
165 pFont->setPosition(PME_DEBUG_PANEL_GHOST_X, iDebugStartY + 16);
166 pFont->render(sDebug);
167
168 // Speed and GoingOutTrigger
169 sDebug = "Speed: ";
170 mC64.ITool().intToStrDec(iSpeed, sTmp);
171 sDebug += sTmp; sDebug += " - GoingOut: ";
172 mC64.ITool().intToStrDec(iGoingOutTrigger, sTmp);
173 sDebug += sTmp;
174 pFont->setPosition(PME_DEBUG_PANEL_GHOST_X, iDebugStartY + 32);
175 pFont->render(sDebug);
176
177 // Target
178 mC64.ITool().intToStrDec(pointTarget.iX, sTmp);
179 sDebug = "Target: [";
180 sDebug += sTmp;
181 sDebug += ",";
182 mC64.ITool().intToStrDec(pointTarget.iY, sTmp);
183 sDebug += sTmp;
184 sDebug += "]";
185 pFont->setPosition(PME_DEBUG_PANEL_GHOST_X, iDebugStartY + 48);
186 pFont->render(sDebug);
187
188 // State
189 pCurrentState->getName(sDebug);
190 sDebug = "State: " + sDebug;
191 pFont->setPosition(PME_DEBUG_PANEL_GHOST_X, iDebugStartY + 64);
192 pFont->render(sDebug);
193
194 // Brain name
195 if(BrainsFactory::Instance().getName(idBrain, sDebug) == -1) sDebug = "Empty";
196 sDebug = "Brain: " + sDebug;
197 pFont->setPosition(PME_DEBUG_PANEL_GHOST_X, iDebugStartY + 80);
198 pFont->render(sDebug);
199
200 return 0;
201}
202
203// Message for going to Init state.
204Sint32 Ghost::msgGoInit()
205{
206 // Call base method
207 Actor::msgGoInit();
208 stateChange(pStateInit);
209 return 0;
210}
211
212// Ghosts react changing to evading state.
213Sint32 Ghost::msgPelletPowerEaten(Sint32 iX, Sint32 iY)
214{
215 // Save temporal evasion target (current position of PacMan).
216 // It is transformed to a valid position on the evading state
217 pointEvadingTarget.iX = iX;
218 pointEvadingTarget.iY = iY;
219
220 // Do not affect us if we are on Init or Death states
221 if(!stateCurrentCheck(*pStateInit) && !stateCurrentCheck(*pStateDeath)) stateChange(pStateEvading);
222
223 return 0;
224}
225
226// Ghost react changing to death state.
227// return 0: eaten by PacMan. Going death state.
228// return 1: we eat PacMan.
229// return 2: already death, nothing to do but return home
230Sint32 Ghost::msgGhostCollision()
231{
232 // If I am already death, do nothing
233 if(stateCurrentCheck(*pStateDeath)) return 2;
234
235 // If I am evading, I was eaten by PacMan
236 if(stateCurrentCheck(*pStateEvading))
237 {
238 stateChange(pStateDeath);
239 return 0;
240 }
241
242 // Otherwise, we eat PacMan!. We will receive a msgPause() for avoiding all movement till a msgGoInit() arrives.
243 return 1;
244}
245
246// Get Scattering predefined target
247Sint32 Ghost::getScatteringTarget(Sint32& iX, Sint32& iY)
248{
249 iX = pointScatteringTarget.iX;
250 iY = pointScatteringTarget.iY;
251 return 0;
252}
253
254// Given a desired target (could be valid or not), we try to reach it applying our rules:
255// - try to reach target using euclidean distance
256// - no go back (but when a state change it can happen)
257// - keep moving
258// - result is stored on vPath
259Sint32 Ghost::applyMovementRules(Sint32 iTX, Sint32 iTY)
260{
261 Sint32 i, iSelected = 0;
262 double dLowest = 1000.0f;
263 vector<MazePoint> vMovementOptions;
264
265 // 1.Get possible movement options. ToDO: tunnels?
266 pGameField->getMovementOptions(iMazeX, iMazeY, vMovementOptions);
267
268 // 2.Normal mode: can not go back
269 for(i = 0; i < vMovementOptions.size();)
270 {
271 if((vMovementOptions[i].iX == iMazePrevX) && (vMovementOptions[i].iY == iMazePrevY))
272 {
273 vMovementOptions.erase(vMovementOptions.begin() + i);
274 }
275 else ++i;
276 }
277
278 // 3.1.If there is only a possible way, avoid more calcs
279 if(vMovementOptions.size() == 1) iSelected = 0;
280
281 // 3.2.Assign distances to target and select the closest one.
282 // In case of match, we select the first one: it is already prioritized in getMovementOptions()
283 else
284 {
285 for(i = 0; i < vMovementOptions.size(); ++i) euclideanDistance(iTX, iTY, vMovementOptions[i]);
286 for(i = 0; i < vMovementOptions.size(); ++i)
287 {
288 if(dLowest > vMovementOptions[i].dDistance)
289 {
290 dLowest = vMovementOptions[i].dDistance;
291 iSelected = i;
292 }
293 }
294 }
295
296 // 4.Add the selected point to the path
297 vPath.push_back(vMovementOptions[iSelected]);
298 return 0;
299}
300
301// Init State
302// Wait for PME_GETREADY_TIME and then for our starting signal(depending on the number of eaten pellets)
303GhostStateInit::GhostStateInit(const string& sN) : State(sN) { iTime = 0; }
304void GhostStateInit::enter(Actor* pAct)
305{
306 Ghost* pGhost = reinterpret_cast<Ghost*>(pAct);
307
308 // Call our base class method
309 State::enter(pAct);
310
311 // Reset our movemements vars
312 pGhost->eAM = Actor::eActorMoving::AM_NEXT_STEP;
313 pGhost->vPath.clear();
314
315 // Time for waiting in the ghost home but when we came from a death (eaten by PacMan), just wait 1/4. This way it is easier for PacMan
316 iTime = Main::Instance().ITimer().getTicksNow();
317 if(pGhost->statePreviousCheck(*pGhost->pStateDeath)) iTime = iTime - (pGhost->pGameField->messageGetReady(-1) / 4);
318}
319void GhostStateInit::execute(Actor* pAct)
320{
321 Ghost* pGhost = reinterpret_cast<Ghost*>(pAct);
322 Sint32 iTmp;
323
324 // 1.If we received a msgPause(), we avoid to execute anything else till the msgGoInit()
325 if(pGhost->eAM == Actor::eActorMoving::AM_DISABLED) return;
326
327 // 2.Before going out of home, wait for the getready time
328 if(Main::Instance().ITimer().getTicksNow() < (iTime + pGhost->pGameField->messageGetReady(-1))) return;
329
330 // 3.Are we waiting on the ghost home? Then move side to side
331 if(pGhost->pGameField->getEatenPelletsPercent() < pGhost->iGoingOutTrigger)
332 {
333 // Have we reach our current target? Go to the other side (11,14) <-> (16,14)
334 if(pGhost->vPath.size() == 0)
335 {
336 if(pGhost->iMazeX == 11) iTmp = 16;
337 else iTmp = 11;
338 pGhost->pGameField->mapSearch().findPath(pGhost->iMazeX, pGhost->iMazeY, iTmp, 14, pGhost->vPath, PME_STATE_WALKABLE_GHOST);
339 pGhost->pointTarget.iX = iTmp;
340 pGhost->pointTarget.iY = 14;
341 }
342 return;
343 }
344
345 // 4.Time to exit home. Wait for next wall hit
346 if(pGhost->vPath.size() == 0)
347 {
348 pGhost->pGameField->mapSearch().findPath(pGhost->iMazeX, pGhost->iMazeY, 13, 11, pGhost->vPath, PME_STATE_WALKABLE_GHOST);
349 pGhost->pointTarget.iX = 13;
350 pGhost->pointTarget.iY = 11;
351 }
352
353 // 5.Have we reached exit target? then change to chasing
354 if(pGhost->iMazeX == 13 && pGhost->iMazeY == 11)
355 {
356 pGhost->stateChange(pGhost->pStateChasing);
357 }
358}
359
360// Evading State
361GhostStateEvading::GhostStateEvading(const string& sN) : State(sN) { }
362void GhostStateEvading::enter(Actor* pAct)
363{
364 Ghost* pGhost = reinterpret_cast<Ghost*>(pAct);
365
366 // Call our base class method
367 State::enter(pAct);
368
369 // Slowdown the ghost
370 pGhost->iSpeed -= PME_GHOST_SPEED_VARIATION;
371
372 // Set first stage evading sprite
373 pGhost->eSS = Actor::eSpriteState::SS_EVADE_FIRST_STAGE;
374}
375void GhostStateEvading::execute(Actor* pAct)
376{
377 Ghost* pGhost = reinterpret_cast<Ghost*>(pAct);
378 Sint32 iX, iY, iWaveMode;
379
380 // 1.When the empowered time expires...
381 iWaveMode = pGhost->pGameField->getWaveMode();
382 if((iWaveMode != PME_GLOBAL_WAVE_EVADING) && (iWaveMode != PME_GLOBAL_WAVE_EVADING_END))
383 {
384 // go back to previous state
385 pGhost->statePrevious();
386 return;
387 }
388
389 // 2.When 1/4 of ticks left..
390 if(iWaveMode == PME_GLOBAL_WAVE_EVADING_END)
391 {
392 // change to second stage evading sprite
393 pGhost->eSS = Actor::eSpriteState::SS_EVADE_SECOND_STAGE;
394 }
395
396 // 3.Evasion!
397 if(pGhost->vPath.size() == 0)
398 {
399 // pointEvadingTarget has the coordinates were PacMan ate a power pellet so we try to target a distant point
400 iX = Main::Instance().ITool().randWELL() % 6;
401 iX = MAZE_WIDTH - pGhost->pointEvadingTarget.iX + iX - 3; // Randomize a bit the destination
402 iY = Main::Instance().ITool().randWELL() % 6;
403 iY = MAZE_HEIGHT - pGhost->pointEvadingTarget.iY + iY - 3; // Randomize a bit the destination
404 pGhost->pGameField->validateMazePosition(iX, iY); // Validate the maze position, it will find the closest valid position
405 pGhost->pGameField->mapSearch().findPath(pGhost->iMazeX, pGhost->iMazeY, iX, iY, pGhost->vPath, PME_STATE_WALKABLE);
406 pGhost->pointTarget.iX = iX;
407 pGhost->pointTarget.iY = iY;
408 }
409}
410void GhostStateEvading::exit(Actor* pAct)
411{
412 Ghost* pGhost = reinterpret_cast<Ghost*>(pAct);
413
414 // Call our base class method
415 State::exit(pAct);
416
417 // Recover previous speed
418 pGhost->iSpeed += PME_GHOST_SPEED_VARIATION;
419
420 // Set default sprite state
421 pGhost->eSS = Actor::eSpriteState::SS_DEFAULT;
422
423 // Clear movement attributes
424 pGhost->eAM = Actor::eActorMoving::AM_NEXT_STEP;
425 pGhost->vPath.clear();
426}
427
428// Chasing State
429GhostStateChasing::GhostStateChasing(const string& sN) : State(sN) { iTX = iTY = iITX = iITY = 0; bGetNewTarget = true; }
430void GhostStateChasing::enter(Actor* pAct)
431{
432 // Call our base class method
433 State::enter(pAct);
434}
435void GhostStateChasing::execute(Actor* pAct)
436{
437 vector<MazePoint> vIntersection;
438 Ghost* pGhost = reinterpret_cast<Ghost*>(pAct);
439 Sint32 iWaveMode;
440
441 // 1.If we received a msgPause(), we avoid to execute anything else till the msgGoInit()
442 if(pGhost->eAM == Actor::eActorMoving::AM_DISABLED) return;
443
444 // 2.Check global wave mode
445 iWaveMode = pGhost->pGameField->getWaveMode();
446 if(iWaveMode == PME_GLOBAL_WAVE_SCATTERING)
447 {
448 pGhost->stateChange(pGhost->pStateScattering);
449 return;
450 }
451 else if(iWaveMode == PME_GLOBAL_WAVE_EVADING)
452 {
453 pGhost->stateChange(pGhost->pStateEvading);
454 return;
455 }
456
457 // 3.1.When we reach our target, request to get a new target
458 if((pGhost->iMazeX == iTX) && (pGhost->iMazeY == iTY)) bGetNewTarget = true;
459
460 // 3.2.At each intersection, request once to get a new target
461 pGhost->pGameField->getMovementOptions(pGhost->iMazeX, pGhost->iMazeY, vIntersection);
462 if((vIntersection.size() > 2) && ((pGhost->iMazeX != iITX) || (pGhost->iMazeY != iITY)))
463 {
464 iITX = pGhost->iMazeX;
465 iITY = pGhost->iMazeY;
466 bGetNewTarget = true;
467 }
468
469 // 3.3.Call to the ghost brain for getting a new "High-level target" in (iTX,iTY)
470 if(bGetNewTarget)
471 {
472 pGhost->think(iTX, iTY);
473 pGhost->pointTarget.iX = iTX;
474 pGhost->pointTarget.iY = iTY;
475 bGetNewTarget = false;
476 }
477
478 // 4.Keep filling our path applying ghosts movement rules.
479 if(pGhost->vPath.size() == 0)
480 {
481 pGhost->applyMovementRules(iTX, iTY);
482 }
483}
484void GhostStateChasing::exit(Actor* pAct)
485{
486 Ghost* pGhost = reinterpret_cast<Ghost*>(pAct);
487
488 // Call our base class method
489 State::exit(pAct);
490
491 // Clear movement attributes
492 pGhost->eAM = Actor::eActorMoving::AM_NEXT_STEP;
493 pGhost->vPath.clear();
494}
495
496// Scattering State
497GhostStateScattering::GhostStateScattering(const string& sN) : State(sN) {}
498void GhostStateScattering::enter(Actor* pAct)
499{
500 // Call our base class method
501 State::enter(pAct);
502}
503void GhostStateScattering::execute(Actor* pAct)
504{
505 Sint32 iWaveMode;
506 Ghost* pGhost = reinterpret_cast<Ghost*>(pAct);
507
508 // 1.If we received a msgPause(), we avoid to execute anything else till the msgGoInit()
509 if(pGhost->eAM == Actor::eActorMoving::AM_DISABLED) return;
510
511 // 2.Check global wave mode
512 iWaveMode = pGhost->pGameField->getWaveMode();
513 if(iWaveMode == PME_GLOBAL_WAVE_CHASING)
514 {
515 pGhost->stateChange(pGhost->pStateChasing);
516 return;
517 }
518 else if(iWaveMode == PME_GLOBAL_WAVE_EVADING)
519 {
520 pGhost->stateChange(pGhost->pStateEvading);
521 return;
522 }
523
524 // 3.With no path, define a new target and try to reach it. Apply Ghost movement rules.
525 if(pGhost->vPath.size() == 0)
526 {
527 pGhost->pointTarget = pGhost->pointScatteringTarget;
528 pGhost->applyMovementRules(pGhost->pointScatteringTarget.iX, pGhost->pointScatteringTarget.iY);
529 }
530}
531void GhostStateScattering::exit(Actor* pAct)
532{
533 Ghost* pGhost = reinterpret_cast<Ghost*>(pAct);
534
535 // Call our base class method
536 State::exit(pAct);
537
538 // Clear movement attributes
539 pGhost->eAM = Actor::eActorMoving::AM_NEXT_STEP;
540 pGhost->vPath.clear();
541}
542
543// Death State
544GhostStateDeath::GhostStateDeath(const string& sN) : State(sN) {}
545void GhostStateDeath::enter(Actor* pAct)
546{
547 Ghost* pGhost = reinterpret_cast<Ghost*>(pAct);
548
549 // Call our base class method
550 State::enter(pAct);
551
552 // Maximum ghost speed = PacMan speed
553 pGhost->iSpeed = PME_PACMAN_START_SPEED;
554
555 // Set death sprite
556 pGhost->eSS = Actor::eSpriteState::SS_DEATH;
557}
558void GhostStateDeath::execute(Actor* pAct)
559{
560 Ghost* pGhost = reinterpret_cast<Ghost*>(pAct);
561
562 // 1.If we received a msgPause(), we avoid to execute anything else till the msgGoInit()
563 if(pGhost->eAM == Actor::eActorMoving::AM_DISABLED) return;
564
565 // 2.Go home, starting position
566 if(pGhost->vPath.size() == 0)
567 {
568 // 1.1.Have we reached our home?
569 if(pGhost->iMazeX == (pGhost->iStartingPositionX / MAZE_TILE_SIZE) && pGhost->iMazeY == (pGhost->iStartingPositionY / MAZE_TILE_SIZE))
570 pGhost->stateChange(pGhost->pStateInit);
571
572 // 1.2.Get the path to our home
573 else
574 {
575 pGhost->pointTarget.iX = pGhost->iStartingPositionX / MAZE_TILE_SIZE;
576 pGhost->pointTarget.iY = pGhost->iStartingPositionY / MAZE_TILE_SIZE;
577 pGhost->pGameField->mapSearch().findPath(pGhost->iMazeX, pGhost->iMazeY, pGhost->pointTarget.iX, pGhost->pointTarget.iY, pGhost->vPath, PME_STATE_WALKABLE_GHOST);
578 }
579 }
580}
581void GhostStateDeath::exit(Actor* pAct)
582{
583 Ghost* pGhost = reinterpret_cast<Ghost*>(pAct);
584
585 // Call our base class method
586 State::exit(pAct);
587
588 // Recover our speed
589 pGhost->iSpeed = PME_GHOST_START_SPEED;
590
591 // Set default sprite state
592 pGhost->eSS = Actor::eSpriteState::SS_DEFAULT;
593}