Maelstrom: Added lots of Steam achievements

From 88806e7107e6add5862af29fc44d24789d4e6f29 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Wed, 18 Mar 2026 21:51:54 -0700
Subject: [PATCH] Added lots of Steam achievements

---
 game/Maelstrom_Globals.h                      |   1 +
 game/game.cpp                                 |  15 +--
 game/make.cpp                                 |  26 +++--
 game/object.h                                 |  15 +++
 game/objects.cpp                              |   6 +-
 game/objects.h                                | 109 +++++++++++++++---
 game/player.cpp                               |  43 ++++++-
 game/player.h                                 |   7 ++
 game/shinobi.h                                |   4 +
 .../achievements/ACHIEVEMENT_BLUE_MOON.xcf    | Bin 0 -> 40982 bytes
 .../ACHIEVEMENT_BLUE_MOON_BONUS.png           | Bin 0 -> 5861 bytes
 .../ACHIEVEMENT_BLUE_MOON_BONUS_GRAY.png      | Bin 0 -> 3756 bytes
 .../ACHIEVEMENT_BLUE_MOON_GRAVITY.png         | Bin 0 -> 6432 bytes
 .../ACHIEVEMENT_BLUE_MOON_GRAVITY_GRAY.png    | Bin 0 -> 4223 bytes
 .../ACHIEVEMENT_BLUE_MOON_MINE.png            | Bin 0 -> 6559 bytes
 .../ACHIEVEMENT_BLUE_MOON_MINE_GRAY.png       | Bin 0 -> 4188 bytes
 .../ACHIEVEMENT_BLUE_MOON_PRIZE.png           | Bin 0 -> 7026 bytes
 .../ACHIEVEMENT_BLUE_MOON_PRIZE_GRAY.png      | Bin 0 -> 4018 bytes
 .../assets/achievements/ACHIEVEMENT_BONUS.xcf | Bin 51398 -> 54751 bytes
 .../achievements/ACHIEVEMENT_BONUS_0.png      | Bin 0 -> 2320 bytes
 .../achievements/ACHIEVEMENT_BONUS_0_GRAY.png | Bin 0 -> 1665 bytes
 .../assets/achievements/ACHIEVEMENT_ICONS.xcf | Bin 0 -> 195873 bytes
 .../ACHIEVEMENT_ICON_ASTEROID.png             | Bin 0 -> 6389 bytes
 .../ACHIEVEMENT_ICON_ASTEROIDS.png            | Bin 0 -> 7868 bytes
 .../ACHIEVEMENT_ICON_ASTEROIDS_GRAY.png       | Bin 0 -> 4811 bytes
 .../ACHIEVEMENT_ICON_ASTEROID_GRAY.png        | Bin 0 -> 3999 bytes
 .../achievements/ACHIEVEMENT_ICON_BONUS.png   | Bin 0 -> 5524 bytes
 .../ACHIEVEMENT_ICON_BONUS_GRAY.png           | Bin 0 -> 3343 bytes
 .../achievements/ACHIEVEMENT_ICON_GRAVITY.png | Bin 0 -> 5599 bytes
 .../ACHIEVEMENT_ICON_GRAVITY_GRAY.png         | Bin 0 -> 3508 bytes
 .../achievements/ACHIEVEMENT_ICON_LUCK.png    | Bin 0 -> 9075 bytes
 .../ACHIEVEMENT_ICON_LUCK_GRAY.png            | Bin 0 -> 4527 bytes
 .../achievements/ACHIEVEMENT_ICON_MINE.png    | Bin 0 -> 5820 bytes
 .../ACHIEVEMENT_ICON_MINE_GRAY.png            | Bin 0 -> 3594 bytes
 .../achievements/ACHIEVEMENT_ICON_NOVA.png    | Bin 0 -> 5725 bytes
 .../ACHIEVEMENT_ICON_NOVA_GRAY.png            | Bin 0 -> 3788 bytes
 .../achievements/ACHIEVEMENT_ICON_PRIZE.png   | Bin 0 -> 6407 bytes
 .../ACHIEVEMENT_ICON_PRIZE_GRAY.png           | Bin 0 -> 3511 bytes
 .../achievements/ACHIEVEMENT_ICON_SHENOBI.png | Bin 0 -> 4623 bytes
 .../ACHIEVEMENT_ICON_SHENOBI_GRAY.png         | Bin 0 -> 2821 bytes
 .../achievements/ACHIEVEMENT_ICON_SHIP.png    | Bin 0 -> 7407 bytes
 .../ACHIEVEMENT_ICON_SHIP_GRAY.png            | Bin 0 -> 3998 bytes
 ...ng => ACHIEVEMENT_ICON_STEEL_ASTEROID.png} | Bin
 ... ACHIEVEMENT_ICON_STEEL_ASTEROID_GRAY.png} | Bin
 .../achievements/ACHIEVEMENT_STEEL_BALLS.xcf  | Bin 16053 -> 0 bytes
 .../achievements/ACHIEVEMENT_SURVIVOR.xcf     | Bin 0 -> 13808 bytes
 .../achievements/ACHIEVEMENT_SURVIVOR_10.png  | Bin 0 -> 2798 bytes
 .../ACHIEVEMENT_SURVIVOR_10_GRAY.png          | Bin 0 -> 1889 bytes
 .../achievements/ACHIEVEMENT_SURVIVOR_5.png   | Bin 0 -> 2463 bytes
 .../ACHIEVEMENT_SURVIVOR_5_GRAY.png           | Bin 0 -> 1739 bytes
 50 files changed, 187 insertions(+), 39 deletions(-)
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_BLUE_MOON.xcf
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_BLUE_MOON_BONUS.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_BLUE_MOON_BONUS_GRAY.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_BLUE_MOON_GRAVITY.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_BLUE_MOON_GRAVITY_GRAY.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_BLUE_MOON_MINE.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_BLUE_MOON_MINE_GRAY.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_BLUE_MOON_PRIZE.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_BLUE_MOON_PRIZE_GRAY.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_BONUS_0.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_BONUS_0_GRAY.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_ICONS.xcf
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_ICON_ASTEROID.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_ICON_ASTEROIDS.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_ICON_ASTEROIDS_GRAY.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_ICON_ASTEROID_GRAY.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_ICON_BONUS.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_ICON_BONUS_GRAY.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_ICON_GRAVITY.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_ICON_GRAVITY_GRAY.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_ICON_LUCK.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_ICON_LUCK_GRAY.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_ICON_MINE.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_ICON_MINE_GRAY.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_ICON_NOVA.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_ICON_NOVA_GRAY.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_ICON_PRIZE.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_ICON_PRIZE_GRAY.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_ICON_SHENOBI.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_ICON_SHENOBI_GRAY.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_ICON_SHIP.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_ICON_SHIP_GRAY.png
 rename steam/assets/achievements/{ACHIEVEMENT_STEEL_BALLS.png => ACHIEVEMENT_ICON_STEEL_ASTEROID.png} (100%)
 rename steam/assets/achievements/{ACHIEVEMENT_STEEL_BALLS_GRAY.png => ACHIEVEMENT_ICON_STEEL_ASTEROID_GRAY.png} (100%)
 delete mode 100644 steam/assets/achievements/ACHIEVEMENT_STEEL_BALLS.xcf
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_SURVIVOR.xcf
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_SURVIVOR_10.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_SURVIVOR_10_GRAY.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_SURVIVOR_5.png
 create mode 100644 steam/assets/achievements/ACHIEVEMENT_SURVIVOR_5_GRAY.png

diff --git a/game/Maelstrom_Globals.h b/game/Maelstrom_Globals.h
index 50934a08..69b239e0 100644
--- a/game/Maelstrom_Globals.h
+++ b/game/Maelstrom_Globals.h
@@ -114,6 +114,7 @@ extern int	gBoomDelay;
 extern int	gNextBoom;
 extern int	gBoomPhase;
 extern int	gNumRocks;
+extern int	gNumSmallRocksDestroyed;
 extern int	gLastStar;
 extern int	gWhenDone;
 
diff --git a/game/game.cpp b/game/game.cpp
index ee157b7c..d6939297 100644
--- a/game/game.cpp
+++ b/game/game.cpp
@@ -38,6 +38,7 @@ int	gBoomDelay;
 int	gNextBoom;
 int	gBoomPhase;
 int	gNumRocks;
+int	gNumSmallRocksDestroyed;
 int	gLastStar;
 int	gWhenDone;
 int	gDisplayed;
@@ -996,6 +997,7 @@ GamePanelDelegate::BonusCheckSound()
 	if (TheShip->GetBonus() == 0) {
 		DelayAndDraw(SOUND_DELAY);
 		m_state = STATE_BONUS_TAUNT;
+		UnlockSinglePlayerAchievement("ACHIEVEMENT_BONUS_0");
 		return;
 	}
 	if (TheShip->GetBonus() > 10000) {
@@ -1104,12 +1106,11 @@ GamePanelDelegate::BonusHide()
 void
 GamePanelDelegate::NextWave()
 {
-	int i;
-
 	gEnemySprite = NULL;
 
 	/* -- Initialize some variables */
 	gNumRocks = 0;
+	gNumSmallRocksDestroyed = 0;
 	gShakeTime = 0;
 	gFreezeTime = 0;
 
@@ -1117,15 +1118,7 @@ GamePanelDelegate::NextWave()
 		char achievement[32];
 
 		SDL_snprintf(achievement, sizeof(achievement), "ACHIEVEMENT_WAVE_%d", gWave);
-		OBJ_LOOP(i, MAX_PLAYERS) {
-			if (!gPlayers[i]->IsValid()) {
-				continue;
-			}
-
-			if (gPlayers[i]->CanGetSinglePlayerAchievement()) {
-				UnlockAchievement(achievement);
-			}
-		}
+		UnlockSinglePlayerAchievement(achievement);
 	}
 
 	if (gWave != (gGameInfo.wave - 1)) {
diff --git a/game/make.cpp b/game/make.cpp
index c98c9006..79715c7a 100644
--- a/game/make.cpp
+++ b/game/make.cpp
@@ -58,10 +58,12 @@ void MakePrize(void)
 	int	x, y, newsprite, xVel, yVel, rx;
 	int	index, cap;
 
-	if (FastRandom(BLUE_MOON) == 0)
+	if (FastRandom(BLUE_MOON) == 0) {
 		cap = (FastRandom(MOON_FACTOR) + 2) * 2;
-	else
+		UnlockSinglePlayerAchievement("ACHIEVEMENT_BLUE_MOON_PRIZE");
+	} else {
 		cap = 1;
+	}
 	
 	for (index = 0; index < cap; index++) {
 		x = FastRandom(GAME_WIDTH - SPRITES_WIDTH) + SPRITES_WIDTH;
@@ -155,6 +157,7 @@ void MakeNova(void)
 	gSprites[newsprite] = new Nova(x, y);
 
 	SetSteamTimelineEvent(STEAM_TIMELINE_EVENT_NOVA);
+	UnlockSinglePlayerAchievement("ACHIEVEMENT_NOVA");
 }	/* -- MakeNova */
 
 
@@ -168,10 +171,12 @@ void MakeBonus(void)
 	int	index, cap;
 	int	multFact;
 
-	if (FastRandom(BLUE_MOON) == 0)
+	if (FastRandom(BLUE_MOON) == 0) {
 		cap = (FastRandom(MOON_FACTOR) + 2) * 2;
-	else
+		UnlockSinglePlayerAchievement("ACHIEVEMENT_BLUE_MOON_BONUS");
+	} else {
 		cap = 1;
+	}
 
 	for (index = 0; index < cap; index++) {
 		x = FastRandom(GAME_WIDTH - SPRITES_WIDTH) + SPRITES_WIDTH;
@@ -245,10 +250,13 @@ void MakeGravity(void)
 	int	x, y, min_bad_distance;
 	int	index, cap;
 
-	if (FastRandom(BLUE_MOON) == 0)
+	if (FastRandom(BLUE_MOON) == 0) {
 		cap = (FastRandom(MOON_FACTOR) + 2) * 2;
-	else
+		UnlockSinglePlayerAchievement("ACHIEVEMENT_BLUE_MOON_GRAVITY");
+	} else {
 		cap = 1;
+		UnlockSinglePlayerAchievement("ACHIEVEMENT_GRAVITY");
+	}
 	
 	for (index = 0; index < cap; index++) {
 		min_bad_distance = MIN_BAD_DISTANCE;
@@ -299,10 +307,12 @@ void MakeHoming(void)
 	int	x, y;
 	int	index, cap;
 
-	if (FastRandom(BLUE_MOON) == 0)
+	if (FastRandom(BLUE_MOON) == 0) {
 		cap = (FastRandom(MOON_FACTOR) + 2) * 2;
-	else
+		UnlockSinglePlayerAchievement("ACHIEVEMENT_BLUE_MOON_MINE");
+	} else {
 		cap = 1;
+	}
 	
 	for (index = 0; index < cap; index++) {
 		rx = (VEL_FACTOR + (gWave / 6)) * (SCALE_FACTOR);
diff --git a/game/object.h b/game/object.h
index ae65c504..942ddb59 100644
--- a/game/object.h
+++ b/game/object.h
@@ -34,6 +34,21 @@ class Object {
 	virtual int IsPlayer(void) {
 		return(0);
 	}
+	virtual int IsRock(void) {
+		return(0);
+	}
+	virtual int IsSmallRock(void) {
+		return(0);
+	}
+	virtual int IsMediumRock(void) {
+		return(0);
+	}
+	virtual int IsLargeRock(void) {
+		return(0);
+	}
+	virtual int IsExploding(void) {
+		return(Exploding);
+	}
 	virtual int IsGhost(void) {
 		return(0);
 	}
diff --git a/game/objects.cpp b/game/objects.cpp
index c3782692..abef7332 100644
--- a/game/objects.cpp
+++ b/game/objects.cpp
@@ -118,7 +118,7 @@ error("Created a homing mine!\n");
 }
 
 SmallRock::SmallRock(int X, int Y, int xVel, int yVel, int phaseFreq) :
-	Object(X, Y, xVel, yVel, 
+	Rock(X, Y, xVel, yVel, 
 		((xVel > 0) ? gRock3R : gRock3L), phaseFreq)
 {
 	Set_Points(SMALL_ROID_PTS);
@@ -129,7 +129,7 @@ error("+   Small rock! (%d)\n", gNumRocks);
 }
 
 MediumRock::MediumRock(int X, int Y, int xVel, int yVel, int phaseFreq) :
-	Object(X, Y, xVel, yVel, 
+	Rock(X, Y, xVel, yVel,
 		((xVel > 0) ? gRock2R : gRock2L), phaseFreq)
 {
 	Set_Points(MEDIUM_ROID_PTS);
@@ -140,7 +140,7 @@ error("++  Medium rock! (%d)\n", gNumRocks);
 }
 
 LargeRock::LargeRock(int X, int Y, int xVel, int yVel, int phaseFreq) :
-	Object(X, Y, xVel, yVel, 
+	Rock(X, Y, xVel, yVel, 
 		((xVel > 0) ? gRock1R : gRock1L), phaseFreq)
 {
 	Set_Points(BIG_ROID_PTS);
diff --git a/game/objects.h b/game/objects.h
index 45043246..e0b1771a 100644
--- a/game/objects.h
+++ b/game/objects.h
@@ -39,32 +39,39 @@ class Prize : public Object {
 			case 0:
 				/* -- They got machine guns! */
 				ship->SetSpecial(MACHINE_GUNS);
+				UnlockSinglePlayerAchievement("ACHIEVEMENT_PRIZE_MACHINE_GUNS");
 				break;
 			case 1:
 				/* -- They got Air brakes */
 				ship->SetSpecial(AIR_BRAKES);
+				UnlockSinglePlayerAchievement("ACHIEVEMENT_PRIZE_AIR_BRAKES");
 				break;
 			case 2:
 				/* -- They might get Lucky */
 				ship->SetSpecial(LUCKY_IRISH);
+				UnlockSinglePlayerAchievement("ACHIEVEMENT_PRIZE_LUCK");
 				break;
 			case 3:
 				/* -- They triple fire */
 				ship->SetSpecial(TRIPLE_FIRE);
+				UnlockSinglePlayerAchievement("ACHIEVEMENT_PRIZE_TRIPLE_FIRE");
 				break;
 			case 4:
 				/* -- They got long range */
 				ship->SetSpecial(LONG_RANGE);
+				UnlockSinglePlayerAchievement("ACHIEVEMENT_PRIZE_LONG_RANGE");
 				break;
 			case 5:
 				/* -- They got more shields */
 				ship->IncrShieldLevel((MAX_SHIELD/5)+
 						FastRandom(MAX_SHIELD/2));
+				UnlockSinglePlayerAchievement("ACHIEVEMENT_PRIZE_SHIELDS");
 				break;
 			case 6:
 				/* -- Put 'em on ICE */
 				sound->PlaySound(gFreezeSound, 4);
 				gFreezeTime = FREEZE_DURATION;
+				UnlockSinglePlayerAchievement("ACHIEVEMENT_PRIZE_FREEZING");
 				break;
 			case 7:
 				/* Blow up everything */
@@ -84,6 +91,7 @@ class Prize : public Object {
 					gPlayers[i]->CutThrust(SHAKE_DURATION);
 				}
 				gShakeTime = SHAKE_DURATION;
+				UnlockSinglePlayerAchievement("ACHIEVEMENT_PRIZE_EXPLOSION");
 				break;
 		}
 		sound->PlaySound(gGotPrize, 4);
@@ -138,6 +146,14 @@ class Nova : public Object {
 	int BeenTimedOut(void) {
 		if ( ! Exploding ) {
 			int i;
+
+			int was_exploding = 0;
+			OBJ_LOOP(i, gNumSprites) {
+				if (gSprites[i]->IsRock() && gSprites[i]->IsExploding()) {
+					++was_exploding;
+				}
+			}
+
 			sound->PlaySound(gNovaBoom, 5);
 			OBJ_LOOP(i, gNumSprites) {
 				if ( gSprites[i] == this )
@@ -154,6 +170,16 @@ class Nova : public Object {
 				gPlayers[i]->CutThrust(SHAKE_DURATION);
 			}
 			gShakeTime = SHAKE_DURATION;
+
+			int is_exploding = 0;
+			OBJ_LOOP(i, gNumSprites) {
+				if (gSprites[i]->IsRock() && gSprites[i]->IsExploding()) {
+					++is_exploding;
+				}
+			}
+			if (is_exploding > was_exploding && is_exploding == gNumRocks) {
+				UnlockSinglePlayerAchievement("ACHIEVEMENT_SUPERNOVA");
+			}
 		}
 		return(-1);
 	}
@@ -220,6 +246,7 @@ class DamagedShip : public Object {
 		if (!ship->IsGhost()) {
 			ship->IncrLives(1);
 			sound->PlaySound(gSavedShipSound, 4);
+			UnlockSinglePlayerAchievement("ACHIEVEMENT_GOOD_SAMARITAN");
 		}
 		return(1);
 	}
@@ -391,8 +418,52 @@ class Homing : public Object {
 	int target;
 };
 
+class Rock : public Object {
+public:
+	Rock(int X, int Y, int Xvec, int Yvec, Blit *blit, int PhaseTime) : Object(X, Y, Xvec, Yvec, blit, PhaseTime) { }
+
+	virtual int IsRock(void) {
+		return(1);
+	}
+
+	virtual int BeenRunOver(Object *ship) {
+		int was_exploding = ship->IsExploding();
+		int result = Object::BeenRunOver(ship);
+		if ( ship->IsPlayer() && !was_exploding && ship->IsExploding() && gNumRocks == 1 ) {
+			UnlockSinglePlayerAchievement("ACHIEVEMENT_USING_YOUR_NOGGIN");
+		}
+		return result;
+    }
 
-class SmallRock : public Object {
+	virtual int Explode() {
+		int result = Object::Explode();
+
+		if (gNumSmallRocksDestroyed == 0) {
+			int i;
+
+			int hot_mess = 0;
+			OBJ_LOOP(i, gNumSprites) {
+				if (gSprites[i]->IsExploding()) {
+					continue;
+				}
+				if (gSprites[i]->IsSmallRock()) {
+					++hot_mess;
+					continue;
+				}
+				if (gSprites[i]->IsMediumRock() || gSprites[i]->IsLargeRock()) {
+					hot_mess = false;
+					break;
+				}
+			}
+			if (hot_mess >= 3) {
+				UnlockSinglePlayerAchievement("ACHIEVEMENT_HOT_MESS");
+			}
+		}
+		return result;
+	}
+};
+
+class SmallRock : public Rock {
 
 public:
 	SmallRock(int X, int Y, int xVel, int yVel, int phaseFreq);
@@ -400,12 +471,18 @@ class SmallRock : public Object {
 		--gNumRocks;
 	}
 
+	virtual int IsSmallRock(void) {
+		return(1);
+	}
+
 	int Explode() {
 		/* Don't do anything if we're already exploding */
 		if ( Exploding ) {
 			return(0);
 		}
 
+		++gNumSmallRocksDestroyed;
+
 		/* Speed things up. :-) */
 		if ( --gBoomDelay < BOOM_MIN )
 			gBoomDelay = BOOM_MIN;
@@ -413,10 +490,11 @@ class SmallRock : public Object {
 error("-   Small rock! (%d)\n", gNumRocks);
 #endif
 
-		return(Object::Explode());
+		return(Rock::Explode());
 	}
 };
-class MediumRock : public Object {
+
+class MediumRock : public Rock {
 
 public:
 	MediumRock(int X, int Y, int xVel, int yVel, int phaseFreq);
@@ -424,6 +502,10 @@ class MediumRock : public Object {
 		--gNumRocks;
 	}
 
+	virtual int IsMediumRock(void) {
+		return(1);
+	}
+
 	int Explode() {
 		int newrocks;
 		int  newsprite = gNumSprites;
@@ -468,12 +550,12 @@ class MediumRock : public Object {
 error("--  Medium rock! (%d)\n", gNumRocks);
 #endif
 
-		return(Object::Explode());
+		return(Rock::Explode());
 	}
 };
 
 
-class LargeRock : public Object {
+class LargeRock : public Rock {
 
 public:
 	LargeRock(int X, int Y, int xVel, int yVel, int phaseFreq);
@@ -481,6 +563,10 @@ class LargeRock : public Object {
 		--gNumRocks;
 	}
 
+	virtual int IsLargeRock(void) {
+		return(1);
+	}
+
 	int Explode() {
 		int newrocks;
 		int  newsprite = gNumSprites;
@@ -525,7 +611,7 @@ class LargeRock : public Object {
 error("--- Large rock! (%d)\n", gNumRocks);
 #endif
 
-		return(Object::Explode());
+		return(Rock::Explode());
 	}
 };
 
@@ -553,7 +639,6 @@ class SteelRoid : public Object {
     }
 
 	int Explode(void) {
-		int i;
 		int newsprite;
 
 		/* Don't do anything if we're already exploding */
@@ -574,15 +659,7 @@ class SteelRoid : public Object {
 
 			/* Blow up! */
 			case 1:
-				OBJ_LOOP(i, MAX_PLAYERS) {
-					if (!gPlayers[i]->IsValid()) {
-						continue;
-					}
-
-					if (gPlayers[i]->CanGetSinglePlayerAchievement()) {
-						UnlockAchievement("ACHIEVEMENT_STEEL_BALLS");
-					}
-				}
+				UnlockSinglePlayerAchievement("ACHIEVEMENT_STEEL_BALLS");
 				return(Object::Explode());
 
 			/* Turn into a homing mine */
diff --git a/game/player.cpp b/game/player.cpp
index 560084e5..7746b136 100644
--- a/game/player.cpp
+++ b/game/player.cpp
@@ -83,6 +83,7 @@ Player::NewGame(int lives)
 	Frags = 0;
 	special = 0;
 	Ghost = 0;
+	LastWaveDied = 0;
 	NewShip();
 }
 void
@@ -98,7 +99,19 @@ Player::NewWave(void)
 {
 	int i;
 
-	/* If we were exploding, rejuvinate us */
+	/* If we completed the last level with no shields, unlock achievement */
+	if (NoShieldsThisLevel) {
+		UnlockSinglePlayerAchievement("ACHIEVEMENT_IRON_MAN");
+	}
+
+	int NumWavesSurvived = ((gWave - 1) - LastWaveDied);
+	if (NumWavesSurvived == 10) {
+		UnlockSinglePlayerAchievement("ACHIEVEMENT_SURVIVOR_10");
+	} else if (NumWavesSurvived == 5) {
+		UnlockSinglePlayerAchievement("ACHIEVEMENT_SURVIVOR_5");
+	}
+
+	/* If we were exploding, rejuvenate us */
 	if ( Exploding || (!Alive() && Playing) ) {
 		IncrLives(1);
 		NewShip();
@@ -124,6 +137,8 @@ Player::NewWave(void)
 	phase = 0;
 	OBJ_LOOP(i, numshots)
 		KillShot(i);
+
+	NoShieldsThisLevel = (ShieldLevel == 0);
 }
 /* Returns the number of lives left */
 int 
@@ -212,6 +227,7 @@ Player::BeenShot(Object *ship, Shot *shot)
 		return(0);
 	if ( (special & LUCKY_IRISH) && (FastRandom(LUCK_ODDS) == 0) ) {
 		sound->PlaySound(gLuckySound, 4);
+		UnlockSinglePlayerAchievement("ACHIEVEMENT_LUCKY");
 		return(0);
 	}
 	return(Object::BeenShot(ship, shot));
@@ -229,6 +245,7 @@ Player::BeenRunOver(Object *ship)
 		return(0);
 	if ( (special & LUCKY_IRISH) && (FastRandom(LUCK_ODDS) == 0) ) {
 		sound->PlaySound(gLuckySound, 4);
+		UnlockSinglePlayerAchievement("ACHIEVEMENT_LUCKY");
 		return(0);
 	}
 	return(Object::BeenRunOver(ship));
@@ -244,6 +261,7 @@ Player::BeenDamaged(int damage)
 		return(0);
 	if ( (special & LUCKY_IRISH) && (FastRandom(LUCK_ODDS) == 0) ) {
 		sound->PlaySound(gLuckySound, 4);
+		UnlockSinglePlayerAchievement("ACHIEVEMENT_LUCKY");
 		return(0);
 	}
 	return(Object::BeenDamaged(damage));
@@ -299,6 +317,8 @@ Player::Explode(void)
 	if ( Exploding || !Alive() )
 		return(0);
 
+	LastWaveDied = gWave;
+
 	/* Type 1 shrapnel */
 	rx = (SCALE_FACTOR);
 	xVel = yVel = 0;
@@ -510,6 +530,9 @@ printf("\n");
 					WasShielded = 1;
 				}
 				--ShieldLevel;
+				if (ShieldLevel == 0) {
+					UnlockSinglePlayerAchievement("ACHIEVEMENT_SHIELDS_DOWN");
+				}
 			} else if ( ShieldOn & SHIELD_MANUAL ) {
 				sound->PlaySound(gNoShieldSound, 2);
 			}
@@ -817,3 +840,21 @@ void RotatePlayerView()
 			break;
 	}
 }
+
+/* Function to unlock a single player achievement */
+void UnlockSinglePlayerAchievement(const char *achievement)
+{
+	int i;
+
+	OBJ_LOOP(i, MAX_PLAYERS) {
+		if (!gPlayers[i]->IsValid()) {
+			continue;
+		}
+
+		if (gPlayers[i]->CanGetSinglePlayerAchievement()) {
+			UnlockAchievement(achievement);
+			return;
+		}
+	}
+}
+
diff --git a/game/player.h b/game/player.h
index 7731a974..62215a23 100644
--- a/game/player.h
+++ b/game/player.h
@@ -111,6 +111,7 @@ class Player : public Object {
 		ShieldLevel += level;
 		if ( ShieldLevel > MAX_SHIELD )
 			ShieldLevel = MAX_SHIELD;
+		NoShieldsThisLevel = false;
 	}
 	virtual int GetShieldLevel(void) {
 		return(ShieldLevel);
@@ -171,6 +172,7 @@ class Player : public Object {
 	int Playing;
 	int Dead;
 	int Ghost;
+	int LastWaveDied;
 
 	Shot *shots[MAX_SHOTS];
 	int numshots;
@@ -179,6 +181,8 @@ class Player : public Object {
 	Uint8 controlType;
 	Uint32 controlState;
 
+	bool NoShieldsThisLevel = false;
+
 	/* Create a new shot */
 	int MakeShot(int offset);
 	/* Rubout a flying shot */
@@ -207,3 +211,6 @@ Player *GetControlPlayer(Uint8 controlType);
 
 /* Function to switch the displayed player */
 void RotatePlayerView();
+
+/* Function to unlock a single player achievement */
+void UnlockSinglePlayerAchievement(const char *achievement);
diff --git a/game/shinobi.h b/game/shinobi.h
index c41aaf1d..6f05775d 100644
--- a/game/shinobi.h
+++ b/game/shinobi.h
@@ -280,5 +280,9 @@ class LittleShinobi : public Shinobi {
 public:
 	LittleShinobi(int X, int Y) : Shinobi(X, Y, gEnemyShip2, 15) {
 	}
+
+	virtual void IncrFrags(void) {
+		UnlockSinglePlayerAchievement("ACHIEVEMENT_ALIEN_ABDUCTION");
+	}
 };
 
diff --git a/steam/assets/achievements/ACHIEVEMENT_BLUE_MOON.xcf b/steam/assets/achievements/ACHIEVEMENT_BLUE_MOON.xcf
new file mode 100644
index 0000000000000000000000000000000000000000..e8f3d2a4ad0b15e25a462830fccc6b0aafcbb832
GIT binary patch
literal 40982
zcmeHQeQX@ZbzgENt*euKcIcD{<Wy(M3GK)vCDA2mixL$Xtz&>`e0I~sZrYTVZmB~k
zk|B@E$*H}pV*x}`)NS0FQ2f=j6{_?LDBGeaiV{luM-f3SP!v%A=m%IBX#Nn=qA-M@
zEu8D`H#2)TJGZwtyLXmjTSvToZ{E(lot=5}=FQh~wtRZAP(9%n&Kx*+&~cm|1wfAT
z65w_~54nASTaN##;}Gz>1F#Kn67ZvnX1ouA{RZ%Dvk*O0K0PyA8k$`yGa9A$eCVF#
z%GtS6VR>P$eDdC%_xQ6X7UmX~b{6ovN05bq#|9rBJU;mGf<Lz~QyC~694icb?m)ud
zp#uj#H$Fa4IB+inK9xeCOeiELkxFuPd2!}Md49HV@Bp53HWE^Sp_Tb^rBI$fajLY8
zIMoH;UoKUUF~rwsNk${HfbM%3h4$#u!imz!m8H@{3-f+?_OX@uA-_CV8d{v69V%59
z7nUkRb2H18us{dzDbH6*OLH?wR5&Xhdt%}A;!<gOxx6r6cz{Lnh1rvZ`2|pXu%s4G
zon2PZE9KKAMJ~^rDGgPYX6EreOQrdWO0|e6<)@K_nYmJ>Qj%PMVd?Ztr7$qFQdt;)
zY`pc{%E=P)SoUR%kW*f)oQQI&P&<*KFp8-8p~a;IzH0PV<<!dQr{-tMb5S;4@^n3i
zLfJyjz(Y%=nF>ndOXbR`!o!bz@=;`u#dU|Klc!6SnUgb>8C8tEBa>6r({qJ0r6m^I
zk->w*2L=nJ`4bB#QR7DjKmF4`IW#dib#&)snD8i6cCs=vTbQ3YT{<%G6L{<4N2u<@
z$IF%FM@vhO&zxSID-9f-DBy2$1W{Wj5ecVD^Rtyx14j@2_?S)f&y-Kv#s1V%83i>{
z!FvrHJ=jL{V<oiA6~_HER2i9R6aTY~1OrFM4~?{w|Ack?M^7zO7KR_9b$A>N4<)I|
zW}&mql?u>wzg#^sP$~QVKw)KR?#RHYN@elEk&*f3;hB>QPnCwD7$dwrf=V43I665}
zPi@r5XHPE<GvaV;kxh;?>Yi$&{kDHq20r?Ei64AW;wS!8;+3Z*4oVWgIxcbKx{45i
zW^{M?#N5QbiGv4*(d-d+b^vH^Yk<2*Pp#q#n+zbIi3Su6<lL#zRDQ<cuK)<?k7;yH
zqjzcaZjH`^=JSpz@PH|c&-#d>ft<TZ*YE@0r_o`b!F1Yh$GR2>$yE>pmDe4A2^znT
zJ}$AiuHs6-Z^t!>Z+}_h{)Z&K^Y<looM791j_13@(WdKu$6a$by$$z*TOG8Z_d0IT
z-SC(qK$`VEDZ~AaJLYb<o9;zO*mSP~E_BEXm>ch!x8-g^l4Yom@5&r(xEI}P?scZ`
zkaq#k-pD~#?iwUnhVqW<dx(pY*mAFmx470Juj;Pj=^O54lnABP2$0z=zK@cl{LS2!
zdtJyEIqsM{CQ1Tw^IP6^<Tq6qC0;}+@*Ra<A#bdO%)Iw*i$ST3>8&I~_=mFPxQo!m
zrnZxbTW={yJ%Ki~=#Bzk(c4g0?$lcnq&o_H`3_-g>dKu)OPVlRmPI(yil$u0o1n$)
zocp%4F!UyoKFKOv?|-&p`lL*}oYo_?r%{$+i-HC>^+l<9!ggL@uP%F|3}N<3?43oQ
zg!*o@tj@U2EsJ^|-?BQn^X_R`o2t)>wyci0Y+1S5s0RC;{@S1hd!2o?JsR{ogEbxc
zoc*;V$U7tOD7pvcvjTYh?%3fdTtGMiZgHG`mU+GGu7lrv-2(4|J2v7#o0F(()OvUk
zWAKeFXivu3bp1iciiWQVPmdnAzsK=Xgpix7!t=i1uJt2jy0Cbc%icx2`$arkc<-=0
z%za1Pn{^dekLl{htjo@8C0@eKx_aB{iZ*2n?MiqY^>)<(SJYJvHZK9Z+PISh`Pzu1
zLtkybqeHY;7}R<M**ipgh5n{q0aj>@{g)@b0@$Z)9LyrDiSgd19JQtmW9A|p`N6W=
zhhX7-F=E81(S}Cf;p2@jw8EntabonHfXBQw+%>M~uY7kp_F8nS;2T$HMWlD6uGC8&
zo`e`RH0mN1CoFrHsV+m(qI<Oyo_%dZW7^b*9;a2Eg#3g$3G2+L6<JTQwIXZFs1;dH
zH|qwzlhK~SI%tJEsvCNHXvM}gMcqWLobDHs*8OJEQ488<SlE2T@78_rkWRMPUX!&K
z{lFG1>}78=htVzl#you3OdQUQ(f`{N{vUi$Z{00ITDSO^*M)33H%!dlFz9oDyTZ>g
z$B-(#=W3smHjKAcdBb~i4in=6<gN=`jF;uxZity1zn?SFnR!eJPoH_j47+^@+7a|2
z2biUAM~QNT{Iz|IYa2p-kehXP4Yt~bUduthV5MU*tU(dhvJSzviL83v+#0&2(XHfk
zNuyh}jI^3<#@AZmH2U#|t~Xj|XMCyMfk)2`7OAf9DLJZ*1jyqw<V;I#NA6Q{LVb6~
z;HxOzZ$sPae6)4gXuTdMR`bIaaRXWaTZX9ph^6m@yNPj5Xxr;p*!ZsSO-)W4*7DY@
z;Vg?yXqwp~Qt^e(>UzGJg=1*Gvh$keTXLZ7n{}I?Om(R7?il)tx^A}(wb6VJ^L7mg
zw%y-$J~4St(wsx=Gv+k=n?|?MInX`Mp4z@d&_~Yn`=TRnz5#y66p&t3(Ad=Qz=3hw
zO$`+AZrRk3V`dw_!MO%qr&d7`)TV|(xs}d&a2UMsw+e!Q-{2*IoS{Ps|F6U&zb7#_
zHQa$t0}ezTrZk%TDaV_FlGOpj3^x4<cM66#fkHenWNw8q)M$U71LtWxXG#pAV=2g=
zz|ckJCxJqDA#>|P<`i5n%aD3HQ%yhyb7Dn%=fl|X6y#FUdG|^(72Jke>e;2acy#wr
zdPUTNkGGG-;y$Nyd`QPIt}TVFE6A^mnLtCDk4|tyIHU;_;;o4(Egw?bQjo`PuZUVf
zT+0wA9N2YkiODL<ka$O5rCZ1Iil_rArA^2yB5#}64uUe>3!`fng0W|rUO+Cng#=4d
zV)IXGj*ufuM#|yvgzwDvPr(s#*nK0Fw`CtfNmZFDhoO8DC2AY7T4ABdk>NQswdMk#
z$+37mGWp7hShEXB4x7t%%~KKP1n%D2UZ(;3G@AV8aJ0*7mLJVua4VMOV6E`p&DZm;
z4}Q~C*Z^##gC3a+b&^BPqAh5Fy&5JgU<q2`0J5VU@wm3|DJNa@DR9!l?w%L*Hh~<o
z{!BcDEgx6FkHvV;De<<(71nLSfGLFQ5TKuAd5D&bcI8pmifm!=6fG9oEpi->kw0oS
z^B;?mPsonFLN55m6?t7VAUe(bBg}hLC!z}s&C_3Lr@Qd{NE-|AOtdcOl{<lQNTi@6
zBHn&#O9JIyQTN%G+R%O@7VM6Fh*~A(FCv{~2zf%^nya~2T&8ME!<x#T{R+}tq-OSK
z;_P9nQTIH`2HP@o?02jA4tRax8*jnV`(71S46`6nmP2N)kb4DfhB?H>ncQYJ4&E1i
z8kci0)dm}bQcD}2M(wPLO+#BA7fI5FeLNo%ve>4xiPV-M)HnOi%gF6jvCKP`nM2z{
zNmprOp=+ta!YYjkD-yZ(Ie~kN<BlHI0G{|LEP)2lG~XSC<%-2Z>J`Lbn@4+VMIpV9
z+y_!yQ4aPY`sbQnqWm*(;7!%F(2mwM4$su|FDV4wOsiXFd`s;@F0`1LHLF=eQ|Xqe
zi>-AHHgAjme=D39_XpvLF<vw7tDzl1_gmq9-Y@RO!cRPQw~!LH3?-FJU4;br`zYUJ
z3ZANvh+GMaWE(<Xj(4nE?i}keH3!zYN^b;X1+8sBR)<{3_3`#7fku1jga?c73k#^V
zA0dxcF#}J?!|14C%~AH;8jz9`Z9Vp(AO3b0_4=AeCee|EInvNaC6YEL-=d4K{my7D
zr$Jt$$u~D2_8F6h=H#EbsN0;MX#UN~J)Qt<Ikk7IIInaNrzqcxb9?Z=_21!7=pjX~
z0u$8S-eEQW_ryi;!rv+g0)B^Ik@)ze#Gm+eiHrA0%vq!Rj4dwk9x%R)f;%P-G#S@i
zk()f5uU;{SEuGC*Y&=2=x9E+*^KMJCXYvp$$I0*n3T+?T?%iTP7o-v?ueb+sny>r7
zZ2&CPm<MUhs}1e(sp`o&P##jl3-6dqNwU29ArXhlQieSdKg<!*SF>?o?U9eX$b-jm
zie)I+F^RZ`HDEpFk*iiXtUtUrp909rK@A{%pQ!C5s!{9IfPT){Ar(vAjz&zDGJtiC
zo-Acp)@;8MkFo7S$!6`@hx(ncwvwrvHPpVw;%g?XG5b1+#!Rl4#$92ZBeOKH%*IyK
z=mCV$HZ!57KVdG!nf-UWu(=oryN4Otky<P4I)`z;0iH9CxD7D<jbJ-w9Wm<FA&*{1
z2iZynRcmR+eQcc=ySKu5_8M5NfYmOt>}^AMF4JL70bV;i*nZ|91Bdno={Wy?9kY;C
zWl1{W(WAZ$X&6h1jl=B8xQfvkx61VlF}k=WHyS}|XFRUX@YvO++U^7^C(5!TZl1YZ
z&trwE&#|Go@lV&MJpCMDdEv#x;=z`>6zZ7p9a!Q;_F<~@ThgPP{j0juNtk*uf_N=4
zH;D5n&6KS1IEtkU5E}+;Y0QUAp*07Q8p_N=eLzyZZrai}>n5q5ST~qWi>aMXxT))i
zHrnWw%$1<z^|QTATS&}Nsu@nZMa7KYC;U8*v*l|xG;KS5Jj{RMNSGasQ48dIB7QrX
zR)X>wUmUEp3<(bg9;@*0*Sz(#Vfyn(i_N3*+1`3~4*M|7@WWTd><%{biLpX<9zK78
zK8!~Mu|DF35<BE_whnp3)?JxTTD1;&T;=E1ZjKwoE(XZU%o}0ab@s8z)F}JOP4`*?
z#;l}!9rs(|B1-ov?zh5Y@JP5`9E-;x_pk;oj>W)p5*p7IVnf@tBE8kMYem8XQ>`v*
zi(<M)d$JE<X0hIeZ0LRbM2u=Fb~LQLY6loWx1|SJ^N>#sx1>W24iLXB4b7NFFT<6k
zSWM5|P7icld4KB!^k%A|74{(;ebD1r>VvL7ZS_IdnN~Ux@~Io<$WcEQA4i)bQ8SJ?
za%>mYXT2qz!)$jyX2augyZslVO}k;tk$E%>3mO^OV-^X0T%KEpMSo%+VsDEBOFRrF
z*4Qy;3Lif^Po>282^NHX(N*|8PUl=Pf?%JH!!Xct_H#U7#e*&#aq;f-6E-20IWA(Z
zB@+jpfIM1+a)cz#Rc7M2G8JTr<5O~XM@Kw-c1s*f6BeDVk~IuX>5Q9WdTI?;U2SNw
z{{q|6xd+pR=IjG<%nv~%Q&+`~wdU(OJPaKo9!JnSskN9sD>~XL+^IgJo!D~1w&01r
z56_nE?}BZ(q4UC5oJKzCtso6Y={frAWJ>5UVzt6`jh#X-5o&58Ep-oL`6={LN)y|M
zY_v>|W20qy9PDukl*T1c!S(}I+S0JVO?twkmJtgp4qsYu(rStsKdL7f?M3vaeK=W9
z8h2Z1O31g<5@Am_T%k$tc8xJV!V_ILZ}uk4_~s0RdEX41@*BI2<(j%Vqhijum@_g_
zZv&P!;(gGX;&)5|=~V?hRHIhj556}~)$ntp+V9D~)&e293WA_k-k<yr(D<#)ODwLd
zxDxRDS$?h>zh5{m@$6S6{^AED=Bb)HojlK%U^ToIhL1Gm!N$7N@UGj^n5~}9F?JE-
z$pi}hfV?|hj~7c}Z^kt2DNfYH<57+h(F3-^(6Pw+Z_s-`-$JH{#o|eL&sJEJ!Zbgx
z3OO-2ce7MuO1D*MhJ1@sb?-!7Viy9}7h*AFjO95`$33!%lvm7Sbw-|#Nur=zRg8zQ
zCy=Z0wjnWJgEL2zY(vA};m8(Gg0H%6A41uUac-p*Zr1$+oR1M_r{E7KP%KIM7KpoP
zL#sTmb03Zk9iT3wd&~xn(<{w;wxYA|=1AOJLpDdsC<~*nIbf_(o9AIniNW>)OC`b9
zV1*c;^=SYt1gsWPL1U9CuA#t|pjHwnj8k!B9x>L$7@;i<X+=-S70+);L4ROjvEFE0
zMQ#KS&y2xn5z_?Tv!pRA%iQBFk|~ZxL`m>{+tWAe2D%ohBg^{G>o2)R8h0(~L$9-7
z+uiIbd1}D83cJN!3;1XVELj32EiYooU3;{Y2{cBp(pnc~ybR$g%8&C->?M$ENXW)P
zYG^NKnYbT-JJ+&t#EcSJb0a?)ICwgD0q_*cy4+W27h*b;hb#@urDO=N<4I#m0!t)i
z*jvLh`I%RgVL!&rwjq`_;>w)McSsec?ncKj=BZ9|FB3EcA6MlJZdUB!W2}9c1x~`0
z&-=|7Z!6NHjAJqH_Y3*0F`wTmJ?5E^w>)HI#q|86y(C5g&<-&oh{Z1NPwxlBVU-TO
zrx;1l4z{4s4k4d1;G?vq(esE;Md6KUC2i;p%rRaZbugb9gGbl`rf_DD(!{P_yAWf0
z$S?9u?*qHT(ubK2H5!OsS4e3e!u!iTLxFqIhNjOgq+U;^##uv)&DX==>k#zfyAU^Z
zMV6|i=48<gQ@7r&x`H-Mzd-KQPoSWk^b6Sj;X`oUAzhelLHH_FNMN4^DIM~{{7@Ra
z4M@wtLEUp?$6X(^7Z?|cUOO`nZ5g9K*fR9Fv^^gC>}(u<@Sf-4=>K5l2y(J<#*mjS
zycbsNJZV8wghktst5zH}OFNn}%-y*t3;H4Shp{_0RTy=P_h%2pa@-O%mYO59gEGXP
znlvF#cy1`aEu27UOOFc=xcPb*xz{1U+^jiP-pdo1Sc`6j_lWy>e3qW^(qnos9^|*e
z^kBG#o`jP<tYi-idrE}-w+rt<-kJZ_nBEuf$7A?ck-d<pujuRt>I@!S#1Di06q>oe
z1rE03lvbqXrw|j8>k#x{ZgonG^qQ|5h^YhS44_-hSIiuP-+YZcIp~>u*XN(T2U{`v
zA~9dtj)pG4bHh$F^fy>5x1ez%2=U+*$Ks$D<MMjlsd|KLlzz+=&*d6=wFTz<CBW`f
z9cC1(eH_U*UpGbH#dl+w*oU}ZWo(q5cRUs&Ufgeusdbp=jb0m{tV4?`20i#>DOyqv
ztsl9I#cZ*(XsDqU80W2V5494DMXf~I>S3cTk3)KNcGFyiGsAom`d`SwR|_oY=pLcw
zJw;L5V%i#(5`M=N0QEd6LHz5!zS*UjGv&(J!c#LRzA(GAurhxVQ5-n?;)i1pVtK38
zfB$2o0TqWd|0ADT4s&*DbU~v(rqTR(T^K*{zyIN%`u#KDUl2d*fB*YAq}6_BenSf=
zvR*VQPiMX(Zv-D#_>&4hp%MVq(}4cc|44pS9o(&cNQLiJ_yL6<QCP{TmR0z_BtAQ$
z@OKsdzQj+f|6urZzYKqw|K$+BFON$6i&F}JO5x8dydd#2>fb*;vn<2U&M5pvg?~xm
zUsL#-3jeXf-&Xh^6#jRG|65`@AN&QkD}0y22PKxDU=p9D;^*nQ0XtTmK!+rmxBLMd
z5%VKTapAk157)X0kvHA`oO7hsV-5JHinVC^RcCkYU@91>-RaoCQ0;c7n-Ix2Yeugr
z(h0Q{mj4Hxdu#7UFv=#P17RIC4oT_<Cg1zCQ8uacDyuGq`q)hzXM0-lf^9~7$*o#j
z5l-IeWtLTMLcQxIwy{fL96P%g^d5@5t$6bkmPaosYuc3G?Qrk!IDrP}PybK>yVjpk
zKzhFdpq{^zAY47UVGCbMIb{2(6!zu=fY3vGJH5Ma!n^w|JS(erH;A;_Z&i6X8mmzn
z>U~y!SKfV9d|}3MUfrw0EJ5|FmmtE={kdd5_qPiFlfvIs`1=xn#Z$On;X#GR6@FOZ
z&q(~diub(o;+|Jt<T>R<o>N}rIpsy3Q(ojb<t?s#Tgq8eUfI{aCBwg5Qg}t-a|$cZ
z_l5H^{DO-A!e6NHcO?Egw1>a1|Et2^Q}`{3UsUN{QgU8W;qx!b``=Lc{Kg|H{Di_~
zh0jX-vhreIR^IE&Zz%qo62Iaqtm^3%75^1gUavf)?mw>Z35Ayx{))mcD*W3D|B1qX
zqwqf}{D#7BO8lxTany!dTZRN5r>zY47(VEyfsL$P|L1J;A500suJ1X!H@4O8>(Z6-
zJ^*RyK$OR>OMGWPkhoUIe0T7A{mIlI($#L=7;<{Mbfv`GYeT7_D>Z)Wo6au(kO4t%
z+1a%LZWgE^T?B-*2*-?(Cd{`EHS)vGU49n8;VmpQqz8<&{2L2F&$GVXj`C<zcVU9K
zo7Wyl8-f4V-D{e0sS$Sr;naYVs@sTmhx1z9w;xFxIkp~wo9TjWKL|%-J~W%O2#1zw
zFFy311&00GoGdWu_c}Kkf}Uf(ZcF#VuHJ@Cqusm?*LH98I^Dfa)=*a2VFPJX1Inrv
z_7%f5)NuyrAkUfKH`sPb>iMmgoSpvtO%UXcIXgGN&jK~1GeF2QcxIe5VYJKWI8gVb
zNSpT&Mz7Y?v%b%GUF-GJcnCM+M*XjMsytMii1cgVC5Gc{!~2TYMVpnzzTJ%5+=sjq
zWzy7-r0i#c{4w@By4}q(h7)Om+^Q){mz#>FEb(s6r5ikgk|VY(qrEUqmJue?grijA
zz`2xROrwp1GLJC2JF06)>+Tp-O4CQWlEzAf^~yvyM{li0i0%0LELCj(=e+d}jIgqd
z1j960MxRU*j$nxc=dBE59c_G+Y24A>(Vi}?yJJ%+O&<YEI@tDC*+-X|N{l1JEF(&$
z2}h;G)cl6m7UWl=<FhPdJdq~I{aI8}W^aj-RQqgTi@w%Oqucm)bg=uF(`@0=a-<ad
zxp2l8x$eE>3%VC0aq>cpy3*TfG9B!GrsH|<VE24PTXfB7rWxBT^`>x!nro5~GlI8{
zV?O^zhn;W1#x?+JO#Fa^`iO+H44S|n*U{8kS`SEqxa6Xn_S$XMW4ar%oXh;FAD2S8
z+{&fnk0=_*xtlbWd~3k_H2MzGfclafX#8F~F0p>C7lm5ReNBCL<MkiNSiiZm5xX82
ztk=~2*YO_}`Fs7x6dqRisKP&`@Xsq8%70dce?{Tf6#gTHHx>SSh5t?A|CHD<n?^X9
T9P;1uVU)wF0v5?J1<wBiqRG}+

literal 0
HcmV?d00001

diff --git a/steam/assets/achievements/ACHIEVEMENT_BLUE_MOON_BONUS.png b/steam/assets/achievements/ACHIEVEMENT_BLUE_MOON_BONUS.png
new file mode 100644
index 0000000000000000000000000000000000000000..0f15122a6fd3d1b8f376ad1dbf04121f5fff5625
GIT binary patch
literal 5861
zcmZu#c|4Tu*S}}RFvh;`3}Y(_HP$Rc)}oNCS&|rA#!kWzkv+*CW2q=xD35*LvPAZs
z2vLlE=N-?}`@Wy|_q>1H*XKUhx$bja*E#3=J?C>j*S)1d1!so?06?Xssj3eEAmS?s
zfRPeM5zl18x#X^|p#mD~6b&Q_BzDR=$^h^&mg4w6lqe$}XzJ?

(Patch may be truncated, please check the link at the top of this post.)