Maelstrom: Re-add game area zoom mode on phones

From fcfc16a9413f4fc043811b063d4b09f68be205ac Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Tue, 31 Mar 2026 18:02:52 -0700
Subject: [PATCH] Re-add game area zoom mode on phones

This ends up being a much more dynamic experience, and more fun!
---
 game/game.cpp   | 180 ++++++++++++++++++++++++++++++++++++++++++++++--
 game/game.h     |   2 +
 game/init.cpp   |   4 +-
 game/player.cpp |  43 +++++++++++-
 game/player.h   |  10 +++
 5 files changed, 230 insertions(+), 9 deletions(-)

diff --git a/game/game.cpp b/game/game.cpp
index af22f871..ab9cb7cd 100644
--- a/game/game.cpp
+++ b/game/game.cpp
@@ -599,8 +599,28 @@ GamePanelDelegate::UpdateZoom()
 	SDL_GetRenderSafeArea(renderer, &rect);
 	SDL_SetRenderLogicalPresentation(renderer, saved_w, saved_h, saved_mode);
 
-	// We can zoom if we're on a phone or tablet in landscape mode
-	if ((IsPhone() || IsTablet()) && rect.w > rect.h) {
+	// We can zoom if we're on a phone in landscape mode and not local multiplayer
+	bool zoom = false;
+
+	if (IsPhone() && rect.w > rect.h) {
+		int i;
+
+		int local_players = 0;
+		OBJ_LOOP(i, MAX_PLAYERS) {
+			if (!gPlayers[i]->IsValid()) {
+				continue;
+			}
+
+			if (IS_LOCAL_CONTROL(gPlayers[i]->GetControlType())) {
+				++local_players;
+			}
+		}
+		if (local_players == 1) {
+			zoom = true;
+		}
+	}
+
+	if (zoom) {
 		StartZoom(rect);
 	} else {
 		StopZoom();
@@ -610,6 +630,7 @@ GamePanelDelegate::UpdateZoom()
 void
 GamePanelDelegate::StartZoom(const SDL_Rect &rect)
 {
+	SDL_Renderer *renderer = screen->GetRenderer();
 	float scale = (float)GAME_WIDTH / rect.w;
 	int x = (int)SDL_round(rect.x * scale);
 	int y = (int)SDL_round(rect.y * scale);
@@ -617,6 +638,10 @@ GamePanelDelegate::StartZoom(const SDL_Rect &rect)
 	ui->SetPosition(x, y);
 	ui->SetSize(GAME_WIDTH, height);
 
+	if (!m_texture) {
+		m_texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_UNKNOWN, SDL_TEXTUREACCESS_TARGET, GAME_WIDTH, GAME_HEIGHT);
+	}
+
 	m_zoom = true;
 }
 
@@ -626,26 +651,139 @@ GamePanelDelegate::StopZoom()
 	ui->SetPosition(0, 0);
 	ui->SetSize(GAME_WIDTH, GAME_HEIGHT);
 
+	if (m_texture) {
+		SDL_DestroyTexture(m_texture);
+		m_texture = nullptr;
+	}
+
 	m_zoom = false;
 }
 
 void
 GamePanelDelegate::StartZoomedDrawing()
 {
-	screen->SetLogicalSize(GAME_WIDTH, GAME_HEIGHT);
+	SDL_Renderer *renderer = screen->GetRenderer();
+
+	// Don't clip
+	screen->GetClip(&m_savedClip);
+	SDL_Rect clip = m_savedClip;
+	clip.y = 0;
+	clip.x = 0;
+	clip.w = GAME_WIDTH;
+	clip.h = GAME_HEIGHT;
+	screen->ClipBlit(&clip);
+
+	SDL_SetRenderTarget(renderer, m_texture);
+	screen->Clear();
 }
 
 void
 GamePanelDelegate::StopZoomedDrawing()
 {
-	screen->SetLogicalSize(ui->X() + ui->Width() + ui->X(), ui->Y() + ui->Height() + ui->Y());
+	SDL_Renderer *renderer = screen->GetRenderer();
+
+	SDL_SetRenderTarget(renderer, nullptr);
+	SDL_SetRenderLogicalPresentation(renderer, 0, 0, SDL_LOGICAL_PRESENTATION_DISABLED);
+
+	int w = 0, h = 0;
+	SDL_GetRenderOutputSize(renderer, &w, &h);
+
+	int cameraX, cameraY;
+	gPlayers[0]->GetCameraPos(&cameraX, &cameraY);
+	GetRenderCoordinates(cameraX, cameraY);
+	cameraX += (SPRITES_WIDTH / 2);
+	cameraY += (SPRITES_WIDTH / 2);
+
+	SDL_Rect src;
+	if (w > h) {
+		int visible_width = (GAME_WIDTH - (2 * SPRITES_WIDTH));
+		float scale = (float)visible_width / w;
+		src.w = visible_width;
+		src.h = (int)SDL_roundf(h * scale);
+		src.x = cameraX - src.w / 2;
+		src.y = cameraY - src.h / 2;
+	} else {
+		int visible_height = (GAME_HEIGHT - (2 * SPRITES_WIDTH));
+		float scale = (float)visible_height / h;
+		src.w = (int)SDL_roundf(w * scale);
+		src.h = visible_height;
+		src.x = cameraX - src.w / 2;
+		src.y = cameraY - src.h / 2;
+	}
+	float minu = (float)src.x / m_texture->w;
+	float minv = (float)src.y / m_texture->h;
+	float maxu = (float)(src.x + src.w) / m_texture->w;
+	float maxv = (float)(src.y + src.h) / m_texture->h;
+
+	SDL_FRect dst;
+	dst.x = 0.0f;
+	dst.y = 0.0f;
+	dst.w = (float)w;
+	dst.h = (float)h;
+
+	SDL_FColor color = { 1.0f, 1.0f, 1.0f, 1.0f };
+	SDL_Vertex verts[6];
+	SDL_Vertex *vert = verts;
+	/* 0 */
+	vert->position.x = dst.x;
+	vert->position.y = dst.y;
+	vert->color = color;
+	vert->tex_coord.x = minu;
+	vert->tex_coord.y = minv;
+	vert++;
+	/* 1 */
+	vert->position.x = dst.x + dst.w;
+	vert->position.y = dst.y;
+	vert->color = color;
+	vert->tex_coord.x = maxu;
+	vert->tex_coord.y = minv;
+	vert++;
+	/* 2 */
+	vert->position.x = dst.x + dst.w;
+	vert->position.y = dst.y + dst.h;
+	vert->color = color;
+	vert->tex_coord.x = maxu;
+	vert->tex_coord.y = maxv;
+	vert++;
+	/* 0 */
+	vert->position.x = dst.x;
+	vert->position.y = dst.y;
+	vert->color = color;
+	vert->tex_coord.x = minu;
+	vert->tex_coord.y = minv;
+	vert++;
+	/* 2 */
+	vert->position.x = dst.x + dst.w;
+	vert->position.y = dst.y + dst.h;
+	vert->color = color;
+	vert->tex_coord.x = maxu;
+	vert->tex_coord.y = maxv;
+	vert++;
+	/* 3 */
+	vert->position.x = dst.x;
+	vert->position.y = dst.y + dst.h;
+	vert->color = color;
+	vert->tex_coord.x = minu;
+	vert->tex_coord.y = maxv;
+	vert++;
+
+	SDL_SetRenderTextureAddressMode(renderer, SDL_TEXTURE_ADDRESS_WRAP, SDL_TEXTURE_ADDRESS_WRAP);
+	SDL_RenderGeometry(renderer, m_texture, verts, 6, NULL, 0);
+	SDL_SetRenderTextureAddressMode(renderer, SDL_TEXTURE_ADDRESS_AUTO, SDL_TEXTURE_ADDRESS_AUTO);
+
+	SDL_SetRenderLogicalPresentation(renderer, ui->X() + ui->Width() + ui->X(), ui->Y() + ui->Height() + ui->Y(), SDL_LOGICAL_PRESENTATION_LETTERBOX);
+
+	screen->ClipBlit(&m_savedClip);
 }
 
 void
 GamePanelDelegate::DrawBorder()
 {
-	SDL_Rect rect;
+	if (m_zoom) {
+		return;
+	}
 
+	SDL_Rect rect;
 	screen->GetClip(&rect);
 	rect.x -= 1;
 	rect.y -= 1;
@@ -1353,4 +1491,36 @@ void RenderSprite(UITexture *sprite, int x, int y, int w, int h)
 	w = (int)(((float)w * gScrnRect.w) / GAME_WIDTH);
 	h = (int)(((float)h * gScrnRect.h) / GAME_HEIGHT);
 	screen->QueueBlit(sprite->Texture(), x, y, w, h, DOCLIP);
+
+	// Render the other sides of the sprite
+	if (x < 0) {
+		x += GAME_WIDTH;
+		screen->QueueBlit(sprite->Texture(), x, y, w, h, DOCLIP);
+	} else if ((x + w) > GAME_WIDTH) {
+		x -= GAME_WIDTH;
+		screen->QueueBlit(sprite->Texture(), x, y, w, h, DOCLIP);
+	}
+	if (y < 0) {
+		y += GAME_HEIGHT;
+		screen->QueueBlit(sprite->Texture(), x, y, w, h, DOCLIP);
+
+		if (x < 0) {
+			x += GAME_WIDTH;
+			screen->QueueBlit(sprite->Texture(), x, y, w, h, DOCLIP);
+		} else if ((x + w) > GAME_WIDTH) {
+			x -= GAME_WIDTH;
+			screen->QueueBlit(sprite->Texture(), x, y, w, h, DOCLIP);
+		}
+	} else if ((y + h) > GAME_HEIGHT) {
+		y -= GAME_HEIGHT;
+		screen->QueueBlit(sprite->Texture(), x, y, w, h, DOCLIP);
+
+		if (x < 0) {
+			x += GAME_WIDTH;
+			screen->QueueBlit(sprite->Texture(), x, y, w, h, DOCLIP);
+		} else if ((x + w) > GAME_WIDTH) {
+			x -= GAME_WIDTH;
+			screen->QueueBlit(sprite->Texture(), x, y, w, h, DOCLIP);
+		}
+	}
 }
diff --git a/game/game.h b/game/game.h
index 4a9c87b9..0c1a36e8 100644
--- a/game/game.h
+++ b/game/game.h
@@ -104,6 +104,8 @@ class GamePanelDelegate : public UIPanelDelegate
 	} m_state;
 
 	bool m_zoom;
+	SDL_Texture *m_texture = nullptr;
+	SDL_Rect m_savedClip;
 };
 
 /* ----------------------------------------------------------------- */
diff --git a/game/init.cpp b/game/init.cpp
index cf2923fb..3c122f60 100644
--- a/game/init.cpp
+++ b/game/init.cpp
@@ -220,8 +220,8 @@ static void DrawLoadBar()
 
 void SetStar(int which)
 {
-	int x = FastRandom(GAME_WIDTH - 2*SPRITES_WIDTH) + SPRITES_WIDTH;
-	int y = FastRandom(GAME_HEIGHT - 2*SPRITES_WIDTH) + SPRITES_WIDTH;
+	int x = FastRandom(GAME_WIDTH);
+	int y = FastRandom(GAME_HEIGHT);
 
 	gTheStars[which]->xCoord = x;
 	gTheStars[which]->yCoord = y;
diff --git a/game/player.cpp b/game/player.cpp
index 78a17311..024cc411 100644
--- a/game/player.cpp
+++ b/game/player.cpp
@@ -138,6 +138,8 @@ Player::NewWave(void)
 	Shooting = 0;
 	WasShooting = 0;
 	Rotating = 0;
+	CameraX = x;
+	CameraY = y;
 	phase = 0;
 	OBJ_LOOP(i, numshots)
 		KillShot(i);
@@ -179,6 +181,8 @@ Player::NewShip(void)
 	phasetime = NO_PHASE_CHANGE;
 	Dead = 0;
 	Exploding = 0;
+	CameraX = x;
+	CameraY = y;
 	Set_TTL(-1);
 	if ( ! gGameInfo.IsDeathmatch() ) {
 		if (Lives > 0) {
@@ -396,10 +400,11 @@ Player::ShotHit(Rect *hitRect)
 	}
 	return(NULL);
 }
+
 int 
 Player::Move(int Freeze)
 {
-	int i;
+	int i, result;
 
 	/* Move and time out old shots */
 #ifdef SERIOUS_DEBUG
@@ -456,6 +461,8 @@ printf("\n");
 
 	/* Check to see if we are dead... */
 	if ( Dead ) {
+		UpdateCamera();
+
 		if ( --Dead == 0 ) {  // New Chance at Life!
 			if ( NewShip() < 0 ) {
 				/* Game Over */
@@ -549,7 +556,39 @@ printf("\n");
 		} else
 			WasShielded = 0;
 	}
-	return(Object::Move(Freeze));
+
+	result = Object::Move(Freeze);
+
+	UpdateCamera();
+
+	return result;
+}
+
+void
+Player::UpdateCamera()
+{
+	if ( Dead ) {
+		// Pan the camera over to our new position
+		const float CAMERA_SPEED = (float)(6 << SPRITE_PRECISION);
+		float deltaX = (float)(x - CameraX);
+		float deltaY = (float)(y - CameraY);
+		float length = SDL_sqrtf(deltaX * deltaX + deltaY * deltaY);
+		float velocityX = (deltaX / length) * CAMERA_SPEED;
+		float velocityY = (deltaY / length) * CAMERA_SPEED;
+		if (SDL_fabs(velocityX) < SDL_fabs(deltaX)) {
+			CameraX += (int)SDL_truncf(velocityX);
+		} else {
+			CameraX = x;
+		}
+		if (SDL_fabs(velocityY) < SDL_fabs(deltaY)) {
+			CameraY += (int)SDL_truncf(velocityY);
+		} else {
+			CameraY = y;
+		}
+	} else {
+		CameraX = x;
+		CameraY = y;
+	}
 }
 
 Uint8 
diff --git a/game/player.h b/game/player.h
index 1d6e287c..fefa4fe8 100644
--- a/game/player.h
+++ b/game/player.h
@@ -167,6 +167,11 @@ class Player : public Object {
 	}
 	bool CanGetAchievement();
 
+	void GetCameraPos(int *X, int *Y) {
+		*X = CameraX;
+		*Y = CameraY;
+	}
+
 private:
 	int Valid;
 	int Index;
@@ -204,6 +209,11 @@ class Player : public Object {
 
 	bool NoShieldsThisLevel = false;
 
+	int CameraX;
+	int CameraY;
+
+	void UpdateCamera();
+
 	/* Create a new shot */
 	int MakeShot(int offset);
 	/* Rubout a flying shot */