[Scummvm-git-logs] scummvm master -> 091b14801bae7f9bf159b54b788c87b743cf4c29

neuromancer noreply at scummvm.org
Wed Jul 15 13:57:22 UTC 2026


This automated email contains information about 5 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
6c361c3549 HYPNO: completed KO localization for wetlands
394e18ae9c HYPNO: added missing sounds in boyz
581bdc81ed SCUMM: RA: improved rapid fire support for both games
daf0923d3a SCUMM: RA2: properly show difficulty from every pilot
091b14801b SCUMM: RA2: scale crossair for some levels


Commit: 6c361c35495e1905678040f27e359255b027924c
    https://github.com/scummvm/scummvm/commit/6c361c35495e1905678040f27e359255b027924c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-15T15:55:56+02:00

Commit Message:
HYPNO: completed KO localization for wetlands

Changed paths:
    engines/hypno/hypno.h
    engines/hypno/wet/arcade.cpp
    engines/hypno/wet/hard.cpp
    engines/hypno/wet/wet.cpp


diff --git a/engines/hypno/hypno.h b/engines/hypno/hypno.h
index ac7de039d2d..c7479e06a8e 100644
--- a/engines/hypno/hypno.h
+++ b/engines/hypno/hypno.h
@@ -527,6 +527,7 @@ private:
 	uint16 getNextChar(const Common::String &str, uint32 &c);
 	void drawGlyph(const Common::BitArray &font, int x, int y, int bitoffset, int width, int height, int pitch, uint32 color, bool invert);
 	void drawKoreanChar(uint16 chr, int &curx, int y, uint32 color);
+	void drawStatLine(const Common::String &name, const Common::String &value, int y, uint32 color);
 	void runMainMenu(Code *code);
 	void runLevelMenu(Code *code);
 	void runCheckLives(Code *code);
diff --git a/engines/hypno/wet/arcade.cpp b/engines/hypno/wet/arcade.cpp
index 74be3622e4e..bd4dd370119 100644
--- a/engines/hypno/wet/arcade.cpp
+++ b/engines/hypno/wet/arcade.cpp
@@ -400,6 +400,18 @@ bool WetEngine::checkTransition(ArcadeTransitions &transitions, ArcadeShooting *
 }
 
 
+// Fixed columns instead of the original "%-20s = ...": that padding counts
+// bytes, so a Korean label (2 bytes, 9px per glyph) drifts the '=' column.
+// Korean labels use the 9px G9A font, spaces included; values stay scifi08.
+void WetEngine::drawStatLine(const Common::String &name, const Common::String &value, int y, uint32 color) {
+	const int labelX = 60;
+	const int valueX = labelX + 21 * 7;
+	const bool korean = (_language == Common::KO_KOR);
+
+	drawString(korean ? "g9a.syf" : "scifi08.fgx", getLocalizedString(name), labelX, korean ? y - 1 : y, 0, color);
+	drawString("scifi08.fgx", value, valueX, y, 0, color);
+}
+
 void WetEngine::runAfterArcade(ArcadeShooting *arc) {
 	_checkpoint = _currentLevel;
 
@@ -436,15 +448,22 @@ void WetEngine::runAfterArcade(ArcadeShooting *arc) {
 		Common::Event event;
 		while (!shouldQuit() && !skip) {
 
+			const Common::String pts = getLocalizedString("points");
+
 			drawImage(*frame, 0, 0, false);
-			drawString("scifi08.fgx", Common::String::format("Lives : %d", _lives), 36, 2, 0, c);
-			drawString("scifi08.fgx", Common::String::format("%-20s = %7d", "SHOTS FIRED", _stats.shootsFired), 60, 46, 0, c);
-			drawString("scifi08.fgx", Common::String::format("%-20s = %7d", "ENEMY TARGETS", _stats.enemyTargets), 60, 56, 0, c);
-			drawString("scifi08.fgx", Common::String::format("%-20s = %7d", "TARGETS DESTROYED", _stats.targetsDestroyed), 60, 66, 0, c);
-			drawString("scifi08.fgx", Common::String::format("%-20s = %7d", "TARGETS MISSED", _stats.targetsMissed), 60, 76, 0, c);
-			drawString("scifi08.fgx", Common::String::format("%-20s = %5d %%", "KILL RATIO", killRatio()), 60, 86, 0, c);
-			drawString("scifi08.fgx", Common::String::format("%-20s = %5d %%", "ACCURACY", accuracyRatio()), 60, 96, 0, c);
-			drawString("scifi08.fgx", Common::String::format("%-20s = %5d %%", "ENERGY", _health), 60, 106, 0, c);
+			if (_language == Common::KO_KOR) {
+				drawString("g9a.syf", getLocalizedString("lives"), 36, 1, 0, c);
+				drawString("scifi08.fgx", Common::String::format(": %d", _lives), 36 + 3 * 9, 2, 0, c);
+			} else {
+				drawString("scifi08.fgx", Common::String::format("%s : %d", getLocalizedString("lives").c_str(), _lives), 36, 2, 0, c);
+			}
+			drawStatLine("shotsFired", Common::String::format("= %7d", _stats.shootsFired), 46, c);
+			drawStatLine("enemyTargets", Common::String::format("= %7d", _stats.enemyTargets), 56, c);
+			drawStatLine("targetsDestroyed", Common::String::format("= %7d", _stats.targetsDestroyed), 66, c);
+			drawStatLine("targetsMissed", Common::String::format("= %7d", _stats.targetsMissed), 76, c);
+			drawStatLine("killRatio", Common::String::format("= %5d %%", killRatio()), 86, c);
+			drawStatLine("accuracy", Common::String::format("= %5d %%", accuracyRatio()), 96, c);
+			drawStatLine("health", Common::String::format("= %5d %%", _health), 106, c);
 
 			while (g_system->getEventManager()->pollEvent(event)) {
 				// Events
@@ -457,8 +476,8 @@ void WetEngine::runAfterArcade(ArcadeShooting *arc) {
 				case Common::EVENT_LBUTTONDOWN:
 				case Common::EVENT_KEYDOWN:
 					bonusCounter = _bonus;
-					drawString("scifi08.fgx", Common::String::format("%-20s = %3d pts", "BONUS", _bonus), 60, 116, 0, c);
-					drawString("scifi08.fgx", Common::String::format("%-20s = %3d pts", "SCORE", _score), 60, 126, 0, c);
+					drawStatLine("bonus", Common::String::format("= %3d %s", _bonus, pts.c_str()), 116, c);
+					drawStatLine("score", Common::String::format("= %3d %s", _score, pts.c_str()), 126, c);
 					skip = true;
 					break;
 				default:
@@ -469,13 +488,13 @@ void WetEngine::runAfterArcade(ArcadeShooting *arc) {
 			if (bonusCounter < _bonus) {
 				bonusCounter++;
 				scoreCounter++;
-				drawString("scifi08.fgx", Common::String::format("%-20s = %3d pts", "BONUS", bonusCounter), 60, 116, 0, c);
-				drawString("scifi08.fgx", Common::String::format("%-20s = %3d pts", "SCORE", scoreCounter), 60, 126, 0, c);
+				drawStatLine("bonus", Common::String::format("= %3d %s", bonusCounter, pts.c_str()), 116, c);
+				drawStatLine("score", Common::String::format("= %3d %s", scoreCounter, pts.c_str()), 126, c);
 			}
 
 			extraLife |= checkScoreMilestones(scoreCounter); // This increase the number of lives, if necessary
 			if (extraLife) {
-				drawString("scifi08.fgx", "EXTRA LIFE", 164, 140, 0, kHypnoColorRed);
+				drawString("scifi08.fgx", getLocalizedString("extraLife"), 164, 140, 0, kHypnoColorRed);
 			}
 
 			drawScreen();
diff --git a/engines/hypno/wet/hard.cpp b/engines/hypno/wet/hard.cpp
index 9ff212713a9..2f131d4a434 100644
--- a/engines/hypno/wet/hard.cpp
+++ b/engines/hypno/wet/hard.cpp
@@ -378,7 +378,7 @@ Common::String WetEngine::getLocalizedString(const Common::String &name) {
 		case Common::ES_ESP:
 			return "ENERGIA";
 		case Common::KO_KOR:
-			return "\xb5\x41\x90\xe1\xbb\xa1"; // ì²´ë ¥ (health)
+			return "\xb5\x41\x90\xe1\xbb\xa1"; // 에너지 (energy)
 		default:
 			return "ENERGY";
 		}
@@ -409,7 +409,7 @@ Common::String WetEngine::getLocalizedString(const Common::String &name) {
 		case Common::ES_ESP:
 			return "BLANCO FIJADO";
 		case Common::KO_KOR:
-			return "\xa1\xa2\xce\x61\x20\xbb\xe1\xaf\x81"; // 목표물포착 (target acquired)
+			return "\xa1\xa2\xce\x61\xa2\x89\x20\xcd\xa1\xc0\x62"; // 목표물 포착 (target acquired)
 		default:
 			return "TARGET ACQUIRED";
 		}
@@ -420,10 +420,75 @@ Common::String WetEngine::getLocalizedString(const Common::String &name) {
 		case Common::ES_ESP:
 			return "ELIGE DIRECCION";
 		case Common::KO_KOR:
-			return "\xa4\x77\xd0\xb7\xac\xe5\x00\x00\xc8\x82"; // 전향선택 (choose direction)
+			return "\xa4\x77\xd0\xb7\x20\xac\xe5\xc8\x82"; // 방향 선택 (choose direction)
 		default:
 			return "CHOOSE DIRECTION";
 		}
+	} else if (name == "shotsFired") {
+		switch (_language) {
+		case Common::KO_KOR:
+			return "\xa2\x81\x8b\xa1\x20\xa4\x69\xac\x61"; // 무기 발사
+		default:
+			return "SHOTS FIRED";
+		}
+	} else if (name == "enemyTargets") {
+		switch (_language) {
+		case Common::KO_KOR:
+			return "\xb8\xe2\xb7\x81\x20\xa1\xa2\xce\x61\xa2\x89"; // 적의 목표물
+		default:
+			return "ENEMY TARGETS";
+		}
+	} else if (name == "targetsDestroyed") {
+		switch (_language) {
+		case Common::KO_KOR:
+			return "\xa1\xa2\xce\x61\xa2\x89\x20\xb9\x41\x88\xe1"; // 목표물 제거
+		default:
+			return "TARGETS DESTROYED";
+		}
+	} else if (name == "targetsMissed") {
+		switch (_language) {
+		case Common::KO_KOR:
+			return "\x91\xbd\xc3\xa5\x20\xa1\xa2\xce\x61\xa2\x89"; // 놓친 목표물
+		default:
+			return "TARGETS MISSED";
+		}
+	} else if (name == "killRatio") {
+		switch (_language) {
+		case Common::KO_KOR:
+			return "\xa1\x77\xba\x97\xb7\x49"; // 명중율
+		default:
+			return "KILL RATIO";
+		}
+	} else if (name == "accuracy") {
+		switch (_language) {
+		case Common::KO_KOR:
+			return "\xb8\xf7\xd1\xc2\x95\xa1"; // 정확도
+		default:
+			return "ACCURACY";
+		}
+	} else if (name == "bonus") {
+		switch (_language) {
+		case Common::KO_KOR:
+			return "\xc2\x81\x88\x61\xb8\xf1\xae\x81"; // 추가점수
+		default:
+			return "BONUS";
+		}
+	} else if (name == "points") {
+		switch (_language) {
+		case Common::KO_KOR:
+			return "\xb8\xf1"; // 점
+		default:
+			return "pts";
+		}
+	} else if (name == "extraLife") {
+		return "EXTRA LIFE";
+	} else if (name == "lives") {
+		switch (_language) {
+		case Common::KO_KOR:
+			return "\xac\x97\x20\xa1\x77"; // 생 명
+		default:
+			return "Lives";
+		}
 	} else
 		error("Invalid string name to localize: %s", name.c_str());
 }
diff --git a/engines/hypno/wet/wet.cpp b/engines/hypno/wet/wet.cpp
index f951e8b3ce2..c15411d576a 100644
--- a/engines/hypno/wet/wet.cpp
+++ b/engines/hypno/wet/wet.cpp
@@ -684,7 +684,8 @@ void WetEngine::drawKoreanChar(uint16 chr, int &curx, int y, uint32 color) {
 	int mididx = mididxlut[mid];
 	int finidx = fin >= 0x12 ? fin - 2 : fin - 1;
 
-	if (initidx < 0 || initidx > 19 || mididx < 0 || mididx > 21 || finidx < 0 || finidx >= 27)
+	// finidx 0 is "no final", 1..27 the final consonants: ã…Ž is 27, so 28 is the bound
+	if (initidx < 0 || initidx > 19 || mididx < 0 || mididx > 21 || finidx < 0 || finidx >= 28)
 		return;
 
 	const int mid_to_init_lut[32] = {


Commit: 394e18ae9c09031a53aaed488d18bb2a2d62937c
    https://github.com/scummvm/scummvm/commit/394e18ae9c09031a53aaed488d18bb2a2d62937c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-15T15:55:56+02:00

Commit Message:
HYPNO: added missing sounds in boyz

Changed paths:
    engines/hypno/boyz/boyz.cpp
    engines/hypno/boyz/hard.cpp
    engines/hypno/boyz/scene.cpp
    engines/hypno/hypno.h


diff --git a/engines/hypno/boyz/boyz.cpp b/engines/hypno/boyz/boyz.cpp
index 6b7b7c2a16f..c522f629651 100644
--- a/engines/hypno/boyz/boyz.cpp
+++ b/engines/hypno/boyz/boyz.cpp
@@ -215,6 +215,9 @@ void BoyzEngine::loadAssets() {
 	Transition *territory_1 = new Transition("c19.mi_");
 	territory_1->intros.push_back("warnings/w17s.smk");
 	territory_1->intros.push_back("c1/c1t01.smk");
+	territory_1->music = kTransitionMusic;
+	territory_1->musicStereo = true;
+	territory_1->playMusicDuringIntro = true;
 	_levels["<territory_1>"] = territory_1;
 
 	Transition *warning_1 = new Transition("c14.mi_");
@@ -235,6 +238,9 @@ void BoyzEngine::loadAssets() {
 	Transition *territory_2 = new Transition("c21.mi_");
 	territory_2->intros.push_back("warnings/w13s.smk");
 	territory_2->intros.push_back("c2/c2t01.smk");
+	territory_2->music = kTransitionMusic;
+	territory_2->musicStereo = true;
+	territory_2->playMusicDuringIntro = true;
 	_levels["<territory_2>"] = territory_2;
 
 	loadArcadeLevel("c21.mi_", "c22.mi_", "<retry_menu>", "");
@@ -243,6 +249,9 @@ void BoyzEngine::loadAssets() {
 	Transition *territory_3 = new Transition("<select_c3>");
 	territory_3->intros.push_back("warnings/w14s.smk");
 	territory_3->intros.push_back("c3/c3t01.smk");
+	territory_3->music = kTransitionMusic;
+	territory_3->musicStereo = true;
+	territory_3->playMusicDuringIntro = true;
 	_levels["<territory_3>"] = territory_3;
 
 	loadArcadeLevel("c31.mi_", "<check_c3>", "<retry_menu>", "");
@@ -275,6 +284,9 @@ void BoyzEngine::loadAssets() {
 	Transition *territory_4 = new Transition("c41.mi_");
 	territory_4->intros.push_back("warnings/w15s.smk");
 	territory_4->intros.push_back("c4/c4t01.smk");
+	territory_4->music = kTransitionMusic;
+	territory_4->musicStereo = true;
+	territory_4->playMusicDuringIntro = true;
 	_levels["<territory_4>"] = territory_4;
 
 	loadArcadeLevel("c41.mi_", "c42.mi_", "<retry_menu>", "");
@@ -289,6 +301,9 @@ void BoyzEngine::loadAssets() {
 	territory_5->intros.push_back("warnings/w16s.smk");
 	territory_5->intros.push_back("c5/c5t01.smk");
 	territory_5->intros.push_back("c5/c5intras.smk");
+	territory_5->music = kTransitionMusic;
+	territory_5->musicStereo = true;
+	territory_5->playMusicDuringIntro = true;
 	_levels["<territory_5>"] = territory_5;
 
 	loadArcadeLevel("c51.mi_", "<check_c5>", "<retry_menu>", "");
diff --git a/engines/hypno/boyz/hard.cpp b/engines/hypno/boyz/hard.cpp
index f50aa4f9914..216e935a5e7 100644
--- a/engines/hypno/boyz/hard.cpp
+++ b/engines/hypno/boyz/hard.cpp
@@ -32,6 +32,9 @@
 namespace Hypno {
 
 void BoyzEngine::runCode(Code *code) {
+	if (!isMusicActive())
+		playMusic(kMenuMusic, 22050, true);
+
 	if (code->name == "<main_menu>")
 		runMainMenu(code);
 	else if (code->name == "<difficulty_menu>")
@@ -92,6 +95,7 @@ void BoyzEngine::runMainMenu(Code *code) {
 				if (event.kbd.keycode == Common::KEYCODE_BACKSPACE)
 					_name.deleteLastChar();
 				else if (event.kbd.keycode == Common::KEYCODE_RETURN && !_name.empty()) {
+					playSound(kMenuChoiceSound, 1);
 					cont = false;
 				} else if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
 					if (runExitMenu())
@@ -100,7 +104,7 @@ void BoyzEngine::runMainMenu(Code *code) {
 				}
 
 				else if (Common::isAlpha(event.kbd.keycode)) {
-					playSound("sound/m_choice.raw", 1);
+					playSound(kMenuChoiceSound, 1);
 					_name = _name + char(event.kbd.keycode - 32);
 				}
 
@@ -171,10 +175,12 @@ bool BoyzEngine::runExitMenu() {
 
 			case Common::EVENT_CUSTOM_ENGINE_ACTION_START:
 				if (event.customType == kActionYes) {
+					playSound(kMenuChoiceSound, 1);
 					quit = true;
 					cont = false;
 
 				} else if (event.customType == kActionNo) {
+					playSound(kMenuChoiceSound, 1);
 					quit = false;
 					cont = false;
 					break;
@@ -183,10 +189,12 @@ bool BoyzEngine::runExitMenu() {
 
 			case Common::EVENT_LBUTTONDOWN:
 				if (yesBox.contains(mousePos)) {
+					playSound(kMenuChoiceSound, 1);
 					quit = true;
 					cont = false;
 					break;
 				} else if (noBox.contains(mousePos)) {
+					playSound(kMenuChoiceSound, 1);
 					quit = false;
 					cont = false;
 					break;
@@ -239,30 +247,38 @@ void BoyzEngine::runDifficultyMenu(Code *code) {
 
 			case Common::EVENT_LBUTTONDOWN:
 				if (chumpBox.contains(mousePos)) {
+					playSound(kMenuChoiceSound, 1);
 					_difficulty = "chump";
 					cont = false;
 				} else if (punkBox.contains(mousePos)) {
+					playSound(kMenuChoiceSound, 1);
 					_difficulty = "punk";
 					cont = false;
 				} else if (badAssBox.contains(mousePos)) {
+					playSound(kMenuChoiceSound, 1);
 					_difficulty = "bad ass";
 					cont = false;
 				} else if (cancelBox.contains(mousePos)) {
+					playSound(kMenuChoiceSound, 1);
 					cont = false;
 				}
 				break;
 
 			case Common::EVENT_CUSTOM_ENGINE_ACTION_START:
 				if (event.customType == kActionDifficultyChump) {
+					playSound(kMenuChoiceSound, 1);
 					_difficulty = "chump";
 					cont = false;
 				} else if (event.customType == kActionDifficultyPunk) {
+					playSound(kMenuChoiceSound, 1);
 					_difficulty = "punk";
 					cont = false;
 				} else if (event.customType == kActionDifficultyBadass) {
+					playSound(kMenuChoiceSound, 1);
 					_difficulty = "bad ass";
 					cont = false;
 				} else if (event.customType == kActionDifficultExit) {
+					playSound(kMenuChoiceSound, 1);
 					cont = false;
 				}
 				break;
@@ -332,36 +348,46 @@ void BoyzEngine::runRetryMenu(Code *code) {
 
 			case Common::EVENT_LBUTTONDOWN:
 				if (retryMissionBox.contains(mousePos)) {
+					playSound(kMenuChoiceSound, 1);
 					_nextLevel = _checkpoint;
 					cont = false;
 				} else if (restartTerritoryBox.contains(mousePos)) {
+					playSound(kMenuChoiceSound, 1);
 					// Restore initial health for the team
 					_health = _maxHealth;
 					_stats = _globalStats;
 					_nextLevel = firstLevelTerritory(_checkpoint);
 					cont = false;
 				} else if (restartMissionBox.contains(mousePos)) {
+					playSound(kMenuChoiceSound, 1);
 					_nextLevel = "<main_menu>";
 					cont = false;
-				} else if (quitBox.contains(mousePos))
+				} else if (quitBox.contains(mousePos)) {
+					playSound(kMenuChoiceSound, 1);
 					quitGame();
+				}
 				break;
 
 			case Common::EVENT_CUSTOM_ENGINE_ACTION_START:
 				if (event.customType == kActionRetry) {
+					playSound(kMenuChoiceSound, 1);
 					_nextLevel = _checkpoint;
 					cont = false;
 				} else if (event.customType == kActionNewMission) {
+					playSound(kMenuChoiceSound, 1);
 					_nextLevel = "<main_menu>";
 					cont = false;
 				} else if (event.customType == kActionRestart) {
+					playSound(kMenuChoiceSound, 1);
 					// Restore initial health for the team
 					_health = _maxHealth;
 					_stats = _globalStats;
 					_nextLevel = firstLevelTerritory(_checkpoint);
 					cont = false;
-				} else if (event.customType == kActionQuit)
+				} else if (event.customType == kActionQuit) {
+					playSound(kMenuChoiceSound, 1);
 					quitGame();
+				}
 				break;
 
 			default:
@@ -461,10 +487,13 @@ void BoyzEngine::endCredits(Code *code) {
 }
 
 void BoyzEngine::showCredits() {
+	stopMusic();
+	playMusic(kCreditsMusic, 22050, true);
 	MVideo c1("intro/sbcred1.smk", Common::Point(0, 0), false, true, false);
 	runIntro(c1);
 	MVideo c2("intro/sbcred2.smk", Common::Point(0, 0), false, true, false);
 	runIntro(c2);
+	stopMusic();
 }
 
 int BoyzEngine::getTerritory(const Common::String &level) {
diff --git a/engines/hypno/boyz/scene.cpp b/engines/hypno/boyz/scene.cpp
index ad9b0970a1d..f6a865e27ac 100644
--- a/engines/hypno/boyz/scene.cpp
+++ b/engines/hypno/boyz/scene.cpp
@@ -194,6 +194,10 @@ bool BoyzEngine::hoverHotspot(Common::Point mousePos) {
 	if (found) {
 		Hotspot *menu = hots->begin();
 		if (menu->type == MakeMenu && !menu->background.empty()) { // Hihghlight
+			if (selected.rect != _lastHighlighted) {
+				_lastHighlighted = selected.rect;
+				playSound(kMenuHiliteSound, 1);
+			}
 			Graphics::Surface sub = menu->backgroundFrames[2]->getSubArea(selected.rect);
 			drawImage(*menu->backgroundFrames[1], 0, 0, false);
 			renderHighlights(hots);
@@ -202,6 +206,7 @@ bool BoyzEngine::hoverHotspot(Common::Point mousePos) {
 		}
 		return true;
 	}
+	_lastHighlighted = Common::Rect();
 	return false;
 }
 
diff --git a/engines/hypno/hypno.h b/engines/hypno/hypno.h
index c7479e06a8e..ee0c69b40bd 100644
--- a/engines/hypno/hypno.h
+++ b/engines/hypno/hypno.h
@@ -635,6 +635,15 @@ private:
 	const Graphics::Font *_font;
 };
 
+// SOUND.LIB ships four music tracks that no level script references: the menus
+// and the territory approach videos use them. m_choice/m_hilite are the menu
+// click and highlight sounds (as in Wetlands).
+static const char *const kMenuChoiceSound = "sound/m_choice.raw";
+static const char *const kMenuHiliteSound = "sound/m_hilite.raw";
+static const char *const kMenuMusic = "sound/46-g.raw";
+static const char *const kTransitionMusic = "sound/45.raw";
+static const char *const kCreditsMusic = "sound/42.raw";
+
 class BoyzEngine : public HypnoEngine {
 public:
 	BoyzEngine(OSystem *syst, const ADGameDescription *gd);
@@ -687,6 +696,7 @@ public:
 	void saveProfile(const Common::String &name, int levelId);
 
 	private:
+	Common::Rect _lastHighlighted;
 	void renderHighlights(Hotspots *hs);
 	void waitForUserClick(uint32 timeout);
 	int pickABox();


Commit: 581bdc81ed52ff748b4639efc1d4277ddb27b5ba
    https://github.com/scummvm/scummvm/commit/581bdc81ed52ff748b4639efc1d4277ddb27b5ba
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-15T15:55:56+02:00

Commit Message:
SCUMM: RA: improved rapid fire support for both games

Changed paths:
    engines/scumm/insane/rebel1/iact.cpp
    engines/scumm/insane/rebel1/menu.cpp
    engines/scumm/insane/rebel1/rebel.cpp
    engines/scumm/insane/rebel1/rebel.h
    engines/scumm/insane/rebel2/menu.cpp
    engines/scumm/insane/rebel2/rebel.cpp
    engines/scumm/insane/rebel2/rebel.h


diff --git a/engines/scumm/insane/rebel1/iact.cpp b/engines/scumm/insane/rebel1/iact.cpp
index 913bcd56006..d874fca97a2 100644
--- a/engines/scumm/insane/rebel1/iact.cpp
+++ b/engines/scumm/insane/rebel1/iact.cpp
@@ -2005,7 +2005,7 @@ void InsaneRebel1::handleGameOpcode5EReset(uint32 param1) {
 	_playerFired = false;
 	_playerSecondaryHeld = false;
 	_fireCooldown = 0;
-	_rapidFirePhase = 0;
+	_rapidFireCounter = 0;
 	memset(_shotSlots, 0, sizeof(_shotSlots));
 	_shotAlternator = 0;
 	_shotSideToggle = false;
@@ -2344,20 +2344,23 @@ void InsaneRebel1::handleGameChunk(int32 subSize, Common::SeekableReadStream &b,
 
 // Called once per frame during interactive rendering.
 void InsaneRebel1::processShot() {
+	// Rapid fire (not in the DOS original, ported from RA2) repeats held
+	// shots every 5th frame from the press.
+	const bool freshPress = _playerFired && _fireCooldown == 0;
+	if (freshPress)
+		_rapidFireCounter = 0;
+
+	bool rapidFireShot = false;
 	if (_optRapidFire) {
-		// fires immediately, while held repeats are gated to phase 0.
-		_rapidFirePhase++;
-		if (_rapidFirePhase > 2)
-			_rapidFirePhase = 0;
+		rapidFireShot = (_rapidFireCounter % 5 == 0) && _playerFired;
+		_rapidFireCounter++;
 	}
 
 	if (!_playerFired)
 		return;
 
-	if (_fireCooldown != 0) {
-		if (!_optRapidFire || _rapidFirePhase != 0)
-			return;
-	}
+	if (_fireCooldown != 0 && !rapidFireShot)
+		return;
 
 	// On-foot mode: only spawn when in aiming stance (dirIndex 11-19) or flags force it.
 	const uint16 effectiveOpcode = getEffectiveGameOpcode();
diff --git a/engines/scumm/insane/rebel1/menu.cpp b/engines/scumm/insane/rebel1/menu.cpp
index 0ec0c19f47c..6947fb4f0a9 100644
--- a/engines/scumm/insane/rebel1/menu.cpp
+++ b/engines/scumm/insane/rebel1/menu.cpp
@@ -1359,6 +1359,8 @@ void InsaneRebel1::runOptionsMenu() {
 				break;
 			case 6:
 				_optRapidFire = !_optRapidFire;
+				ConfMan.setBool("rebel1_rapid_fire", _optRapidFire);
+				ConfMan.flushToDisk();
 				break;
 			case 7:
 				break;
diff --git a/engines/scumm/insane/rebel1/rebel.cpp b/engines/scumm/insane/rebel1/rebel.cpp
index 276660a4b9b..5ec62f2f5cb 100644
--- a/engines/scumm/insane/rebel1/rebel.cpp
+++ b/engines/scumm/insane/rebel1/rebel.cpp
@@ -351,7 +351,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 		!_vm->_mixer->isSoundTypeMuted(Audio::Mixer::kSpeechSoundType);
 	_optTextEnabled = ConfMan.getBool("subtitles");
 	_optControlsYFlip = false;
-	_optRapidFire = true;
+	_optRapidFire = ConfMan.hasKey("rebel1_rapid_fire") ? ConfMan.getBool("rebel1_rapid_fire") : true;
 	_optVolume = _vm->_mixer->getVolumeForSoundType(Audio::Mixer::kMusicSoundType) * 127 / Audio::Mixer::kMaxChannelVolume;
 
 	const struct { const char *name; int32 score; byte difficulty; } kDefaultScores[kHighScoreCount] = {
@@ -380,7 +380,7 @@ InsaneRebel1::InsaneRebel1(ScummEngine_v7 *scumm) : Insane(), _vm(scumm) {
 	_playerFired = false;
 	_playerSecondaryHeld = false;
 	_fireCooldown = 0;
-	_rapidFirePhase = 0;
+	_rapidFireCounter = 0;
 	_gameplayFlags75fe = 0;
 	_gameplayFlags75ff = 0;
 	memset(_shotSlots, 0, sizeof(_shotSlots));
diff --git a/engines/scumm/insane/rebel1/rebel.h b/engines/scumm/insane/rebel1/rebel.h
index b2e5508d197..bf7f6969681 100644
--- a/engines/scumm/insane/rebel1/rebel.h
+++ b/engines/scumm/insane/rebel1/rebel.h
@@ -587,7 +587,8 @@ private:
 	// Secondary held button used by L9 on-foot controls.
 	bool _playerSecondaryHeld;
 	int16 _fireCooldown;
-	int16 _rapidFirePhase;
+	// Rapid-fire cadence counter, re-anchored on each press.
+	int16 _rapidFireCounter;
 	uint16 _gameplayFlags75fe;
 	uint16 _gameplayFlags75ff;
 
diff --git a/engines/scumm/insane/rebel2/menu.cpp b/engines/scumm/insane/rebel2/menu.cpp
index cc1ef27966b..87531c97332 100644
--- a/engines/scumm/insane/rebel2/menu.cpp
+++ b/engines/scumm/insane/rebel2/menu.cpp
@@ -1588,6 +1588,8 @@ int InsaneRebel2::processOptionsInput() {
 					break;
 				case 5:
 					_optRapidFire = !_optRapidFire;
+					ConfMan.setBool("rebel2_rapid_fire", _optRapidFire);
+					ConfMan.flushToDisk();
 					break;
 				case 6:
 					break;
diff --git a/engines/scumm/insane/rebel2/rebel.cpp b/engines/scumm/insane/rebel2/rebel.cpp
index acc4da6582d..81fd27d02d2 100644
--- a/engines/scumm/insane/rebel2/rebel.cpp
+++ b/engines/scumm/insane/rebel2/rebel.cpp
@@ -428,6 +428,7 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	_flyControlMode = 0;
 	_shipFiring = false;
 	_prevMouseButtons = 0;
+	_rapidFireCounter = 0;
 	_shipDirectionH = 2;
 	_shipDirectionV = 3;
 	_shipDirectionIndex = 2 * 7 + 3;
@@ -536,7 +537,8 @@ InsaneRebel2::InsaneRebel2(ScummEngine_v7 *scumm) {
 	// "subtitles" key, which the text-render paths gate on.
 	_optTextEnabled = ConfMan.getBool("subtitles");
 	_optControlsFlipped = false;
-	_optRapidFire = false;
+	// Unlike the original, this defaults to on.
+	_optRapidFire = ConfMan.hasKey("rebel2_rapid_fire") ? ConfMan.getBool("rebel2_rapid_fire") : true;
 	_optVolumeLevel = _vm->_mixer->getVolumeForSoundType(Audio::Mixer::kMusicSoundType) / 2;
 
 	_menuInputActive = false;
@@ -1511,6 +1513,7 @@ int32 InsaneRebel2::processMouse() {
 
 	bool leftPressed = (currentButtons & 1) != 0;
 	bool leftWasPressed = (_prevMouseButtons & 1) != 0;
+	bool leftEdge = leftPressed && !leftWasPressed;
 	bool rightPressed = (currentButtons & 2) != 0;
 	bool rightWasPressed = (_prevMouseButtons & 2) != 0;
 
@@ -1520,7 +1523,7 @@ int32 InsaneRebel2::processMouse() {
 		if (rightPressed && !rightWasPressed) {
 			_rebelControlMode |= 2;
 		}
-		if (leftPressed && !leftWasPressed) {
+		if (leftEdge) {
 			_rebelControlMode |= 1;
 		}
 	} else {
@@ -1542,7 +1545,16 @@ int32 InsaneRebel2::processMouse() {
 	if (autoFire)
 		_rebelControlMode |= 1;
 
-	bool triggerShot = ((_rebelHandler == 25) ? (leftPressed && !leftWasPressed) : leftPressed) || autoFire;
+	// Rapid fire injects a held-button shot every 5th frame from the press.
+	if (leftEdge)
+		_rapidFireCounter = 0;
+	bool rapidFireShot = false;
+	if (!_rebelAutoPlay && _optRapidFire) {
+		rapidFireShot = (_rapidFireCounter % 5 == 0) && leftPressed;
+		_rapidFireCounter++;
+	}
+
+	bool triggerShot = leftEdge || rapidFireShot || autoFire;
 	if (_rebelHandler == 8) {
 		_shipFiring = triggerShot && canShoot;
 	}
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 580cc74101f..8022adf1376 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -810,6 +810,9 @@ public:
 	bool _shipFiring;
 	uint32 _prevMouseButtons;
 
+	// Rapid-fire cadence counter, re-anchored on each press.
+	int16 _rapidFireCounter;
+
 	int16 _shipDirectionIndex;
 	int16 _shipDirectionH;
 	int16 _shipDirectionV;


Commit: daf0923d3ac7efdb3f1176d43e2ab2809cb35261
    https://github.com/scummvm/scummvm/commit/daf0923d3ac7efdb3f1176d43e2ab2809cb35261
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-15T15:55:56+02:00

Commit Message:
SCUMM: RA2: properly show difficulty from every pilot

Changed paths:
    engines/scumm/insane/rebel2/menu.cpp
    engines/scumm/insane/rebel2/rebel.h


diff --git a/engines/scumm/insane/rebel2/menu.cpp b/engines/scumm/insane/rebel2/menu.cpp
index 87531c97332..de13ab5a26d 100644
--- a/engines/scumm/insane/rebel2/menu.cpp
+++ b/engines/scumm/insane/rebel2/menu.cpp
@@ -178,8 +178,7 @@ int InsaneRebel2::processMenuInput() {
 				int mouseY = event.mouse.y;
 				for (int i = 0; i < _menuItemCount; i++) {
 					int itemY = baseY + i * itemSpacing;
-					if (mouseY >= itemY - itemHitTop - 3 * (highRes ? 2 : 1) &&
-							mouseY < itemY - itemHitTop + itemHitHeight - 3 * (highRes ? 2 : 1)) {
+					if (mouseY >= itemY - itemHitTop && mouseY < itemY - itemHitTop + itemHitHeight) {
 						if (i != _menuSelection) {
 							_menuSelection = i;
 							debugC(DEBUG_INSANE, "Menu: Hover selection changed to %d (mouseY=%d)", _menuSelection, mouseY);
@@ -771,8 +770,7 @@ int InsaneRebel2::processChapterSelectInput() {
 
 				for (int i = 0; i < _chapterItemCount; i++) {
 					int itemY = baseY + i * itemSpacing;
-					if (mouseY >= itemY - itemHitTop - 3 * (highRes ? 2 : 1) &&
-							mouseY < itemY - itemHitTop + itemHitHeight - 3 * (highRes ? 2 : 1)) {
+					if (mouseY >= itemY - itemHitTop && mouseY < itemY - itemHitTop + itemHitHeight) {
 						if (i != _chapterSelection) {
 							_chapterSelection = i;
 							_previewOffsetY = _chapterSelection * -50 + 75;
@@ -1246,8 +1244,7 @@ int InsaneRebel2::processLevelSelectInput() {
 			_vm->_mouse.y = event.mouse.y;
 			for (int i = 0; i < itemCount; i++) {
 				int itemY = itemBaseY + i * itemSpacing;
-				if (event.mouse.y >= itemY - itemHitTop - 3 * (highRes ? 2 : 1) &&
-						event.mouse.y < itemY - itemHitTop + itemHitHeight - 3 * (highRes ? 2 : 1)) {
+				if (event.mouse.y >= itemY - itemHitTop && event.mouse.y < itemY - itemHitTop + itemHitHeight) {
 					selection = i;
 					break;
 				}
@@ -1350,6 +1347,40 @@ void InsaneRebel2::drawLevelSelectOverlay(byte *renderBitmap, int pitch, int wid
 	}
 
 	drawMenuItems(renderBitmap, pitch, width, height, pilotItems, _numPilots + 4, _levelSelection);
+
+	drawPilotInfoLines(renderBitmap);
+}
+
+// Difficulty and current chapter of the highlighted pilot, at the bottom left.
+void InsaneRebel2::drawPilotInfoLines(byte *renderBitmap) {
+	if (_pilotMenuMode != kPilotModeSelect || _levelSelection < 0 || _levelSelection >= _numPilots)
+		return;
+
+	SmushPlayer *splayer = ((ScummEngine_v7 *)_vm)->_splayer;
+	if (!splayer)
+		return;
+
+	const PilotData &pilot = _pilots[_levelSelection];
+	const int scale = isHiRes() ? 2 : 1;
+
+	const char *diffFmt = splayer->getString(25);
+	if (!diffFmt || !diffFmt[0])
+		diffFmt = "^f01^c248Difficulty: %s";
+	const char *diffName = splayer->getString(111 + CLIP<int>(pilot.difficulty, 0, 5));
+	if (diffName && strlen(diffName) > 9) {
+		// Skip the ^f01^c005 prefix so the name inherits the line's color.
+		Common::String line = Common::String::format(diffFmt, diffName + 9);
+		drawMenuString(renderBitmap, line.c_str(), 30 * scale, 180 * scale);
+	}
+
+	int chapterIdx = 15;
+	while (chapterIdx > 0 && pilot.damage[chapterIdx] >= 0xFF)
+		chapterIdx--;
+	const char *chapFmt = splayer->getString(26);
+	if (!chapFmt || !chapFmt[0])
+		chapFmt = "^f01^c248Chapter:    %hd";
+	Common::String line = Common::String::format(chapFmt, (short)(chapterIdx + 1));
+	drawMenuString(renderBitmap, line.c_str(), 30 * scale, 190 * scale);
 }
 
 void InsaneRebel2::initDefaultRankings() {
diff --git a/engines/scumm/insane/rebel2/rebel.h b/engines/scumm/insane/rebel2/rebel.h
index 8022adf1376..d3af0ee0c96 100644
--- a/engines/scumm/insane/rebel2/rebel.h
+++ b/engines/scumm/insane/rebel2/rebel.h
@@ -137,6 +137,7 @@ public:
 	int16 _previewOffsetY;
 
 	void drawChapterInfoLine(byte *renderBitmap, int pitch, int width, int height);
+	void drawPilotInfoLines(byte *renderBitmap);
 	Common::String getRankString(int rating);
 	Common::String getChapterPassword(int level, int difficulty);
 


Commit: 091b14801bae7f9bf159b54b788c87b743cf4c29
    https://github.com/scummvm/scummvm/commit/091b14801bae7f9bf159b54b788c87b743cf4c29
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-07-15T15:55:56+02:00

Commit Message:
SCUMM: RA2: scale crossair for some levels

Changed paths:
    engines/scumm/insane/rebel2/render.cpp


diff --git a/engines/scumm/insane/rebel2/render.cpp b/engines/scumm/insane/rebel2/render.cpp
index 21ab71f798b..76e006884d0 100644
--- a/engines/scumm/insane/rebel2/render.cpp
+++ b/engines/scumm/insane/rebel2/render.cpp
@@ -4304,6 +4304,9 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
 		int cw = _smush_iconsNut->getCharWidth(reticleIndex);
 		int ch = _smush_iconsNut->getCharHeight(reticleIndex);
 
+		// Enlarge the tiny third-person/cover reticles; the turret crosshair is already large.
+		const int spriteScale = (_rebelHandler == 0x26) ? 1 : 2;
+
 		const int reticleScale = isHiRes() ? 2 : getRebel2IndicatorScale(width, height);
 		int crosshairX = aimPos.x * reticleScale;
 		int crosshairY = aimPos.y * reticleScale;
@@ -4317,12 +4320,14 @@ void InsaneRebel2::renderCrosshair(byte *renderBitmap, int pitch, int width, int
 			crosshairY += _rebelViewOffsetY * reticleScale;
 		}
 
-		crosshairX += _smush_iconsNut->getCharXOffset(reticleIndex) - cw / 2;
-		crosshairY += _smush_iconsNut->getCharYOffset(reticleIndex) - ch / 2;
+		crosshairX += (_smush_iconsNut->getCharXOffset(reticleIndex) - cw / 2) * spriteScale;
+		crosshairY += (_smush_iconsNut->getCharYOffset(reticleIndex) - ch / 2) * spriteScale;
 
-		renderNutSprite(renderBitmap, pitch, width, height,
+		// reticle glyphs key only on color 0, so transparent231 is false.
+		renderNutSpriteScaledClipped(renderBitmap, pitch, width, height,
+			0, 0, width, height,
 			crosshairX, crosshairY,
-			_smush_iconsNut, reticleIndex);
+			_smush_iconsNut, reticleIndex, false, spriteScale, false);
 	}
 }
 




More information about the Scummvm-git-logs mailing list